@khanacademy/perseus 0.0.1 → 0.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 (503) hide show
  1. package/.babelrc.js +1 -1
  2. package/CHANGELOG.md +7 -0
  3. package/dist/es/index.js +15 -1
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +11934 -0
  6. package/dist/index.js +15 -1
  7. package/dist/index.js.map +1 -1
  8. package/dist/strings.js +148 -1
  9. package/package.json +40 -4
  10. package/src/README.md +7 -0
  11. package/src/__stories__/item-renderer.stories.jsx +27 -0
  12. package/src/__stories__/server-item-renderer.stories.jsx +27 -0
  13. package/src/__testdata__/item-renderer_testdata.js +161 -0
  14. package/src/__testdata__/renderer_testdata.js +123 -0
  15. package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
  16. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
  17. package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
  18. package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
  19. package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
  20. package/src/__tests__/a11y_test.jsx +304 -0
  21. package/src/__tests__/item-renderer_test.jsx +892 -0
  22. package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
  23. package/src/__tests__/mock-widget.jsx +35 -0
  24. package/src/__tests__/perseus-markdown_test.jsx +747 -0
  25. package/src/__tests__/pure-markdown_test.js +552 -0
  26. package/src/__tests__/renderability_test.jsx +481 -0
  27. package/src/__tests__/renderer-api_test.jsx +333 -0
  28. package/src/__tests__/renderer_test.jsx +1792 -0
  29. package/src/__tests__/server-item-renderer_test.jsx +516 -0
  30. package/src/__tests__/test-items/image-item.js +25 -0
  31. package/src/__tests__/test-items/input-number-1-item.js +27 -0
  32. package/src/__tests__/test-items/input-number-2-item.js +39 -0
  33. package/src/__tests__/test-items/table-item.js +30 -0
  34. package/src/__tests__/tex-wrangler_test.js +83 -0
  35. package/src/__tests__/util_test.js +41 -0
  36. package/src/a11y.js +46 -0
  37. package/src/all-widgets.js +9 -0
  38. package/src/article-renderer.jsx +215 -0
  39. package/src/asset-context.js +30 -0
  40. package/src/basic-widgets.js +13 -0
  41. package/src/components/__stories__/button-group.stories.jsx +61 -0
  42. package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
  43. package/src/components/__stories__/graph.stories.jsx +27 -0
  44. package/src/components/__stories__/graphie.stories.jsx +28 -0
  45. package/src/components/__stories__/hud.stories.jsx +36 -0
  46. package/src/components/__stories__/icon.stories.jsx +56 -0
  47. package/src/components/__stories__/image-loader.stories.jsx +60 -0
  48. package/src/components/__stories__/info-tip.stories.jsx +33 -0
  49. package/src/components/__stories__/inline-icon.stories.jsx +39 -0
  50. package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
  51. package/src/components/__stories__/lint.stories.jsx +40 -0
  52. package/src/components/__stories__/math-input.stories.jsx +29 -0
  53. package/src/components/__stories__/math-output.stories.jsx +24 -0
  54. package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
  55. package/src/components/__stories__/number-input.stories.jsx +42 -0
  56. package/src/components/__stories__/possible-answers.stories.jsx +22 -0
  57. package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
  58. package/src/components/__stories__/range-input.stories.jsx +28 -0
  59. package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
  60. package/src/components/__stories__/sortable.stories.jsx +73 -0
  61. package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
  62. package/src/components/__stories__/svg-image.stories.jsx +60 -0
  63. package/src/components/__stories__/tex.stories.jsx +18 -0
  64. package/src/components/__stories__/text-buttons.stories.jsx +51 -0
  65. package/src/components/__stories__/text-input.stories.jsx +34 -0
  66. package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
  67. package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
  68. package/src/components/__stories__/zoomable.stories.jsx +40 -0
  69. package/src/components/__testdata__/sorter_testdata.js +20 -0
  70. package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
  71. package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
  72. package/src/components/__tests__/number-input_test.jsx +112 -0
  73. package/src/components/__tests__/sortable_test.jsx +234 -0
  74. package/src/components/__tests__/sorter_test.jsx +111 -0
  75. package/src/components/__tests__/zoomable_test.jsx +388 -0
  76. package/src/components/button-group.jsx +131 -0
  77. package/src/components/constants.js +15 -0
  78. package/src/components/fixed-to-responsive.jsx +163 -0
  79. package/src/components/graph.jsx +414 -0
  80. package/src/components/graphie-classes.jsx +148 -0
  81. package/src/components/graphie-movables.jsx +342 -0
  82. package/src/components/graphie.jsx +353 -0
  83. package/src/components/highlighting/highlightable-content.jsx +245 -0
  84. package/src/components/highlighting/highlights.js +208 -0
  85. package/src/components/highlighting/ranges.js +299 -0
  86. package/src/components/highlighting/types.js +73 -0
  87. package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
  88. package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
  89. package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
  90. package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
  91. package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
  92. package/src/components/highlighting/ui/types.js +21 -0
  93. package/src/components/highlighting/ui/util.js +166 -0
  94. package/src/components/highlighting/word-indexer.jsx +155 -0
  95. package/src/components/hud.jsx +179 -0
  96. package/src/components/icon.jsx +149 -0
  97. package/src/components/image-loader.jsx +175 -0
  98. package/src/components/info-tip/info-tip.jsx +149 -0
  99. package/src/components/info-tip.jsx +38 -0
  100. package/src/components/inline-icon.jsx +81 -0
  101. package/src/components/input-with-examples.jsx +231 -0
  102. package/src/components/lint.jsx +456 -0
  103. package/src/components/math-input.jsx +282 -0
  104. package/src/components/math-output.jsx +128 -0
  105. package/src/components/multi-button-group.jsx +137 -0
  106. package/src/components/new-tooltip/README.md +110 -0
  107. package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
  108. package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
  109. package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
  110. package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
  111. package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
  112. package/src/components/new-tooltip/__tests__/util_test.js +47 -0
  113. package/src/components/new-tooltip/flexible-position.jsx +182 -0
  114. package/src/components/new-tooltip/new-tooltip.jsx +399 -0
  115. package/src/components/new-tooltip/style-constants.js +13 -0
  116. package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
  117. package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
  118. package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
  119. package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
  120. package/src/components/new-tooltip/types.js +183 -0
  121. package/src/components/new-tooltip/util.js +27 -0
  122. package/src/components/number-input.jsx +253 -0
  123. package/src/components/possible-answers.jsx +36 -0
  124. package/src/components/prop-check-box.jsx +78 -0
  125. package/src/components/range-input.jsx +61 -0
  126. package/src/components/simple-keypad-input.jsx +91 -0
  127. package/src/components/sortable.jsx +872 -0
  128. package/src/components/stub-tag-editor.jsx +53 -0
  129. package/src/components/svg-image.jsx +762 -0
  130. package/src/components/tex-buttons.jsx +322 -0
  131. package/src/components/tex.jsx +70 -0
  132. package/src/components/text-input.jsx +82 -0
  133. package/src/components/text-list-editor.jsx +157 -0
  134. package/src/components/tooltip.jsx +395 -0
  135. package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
  136. package/src/components/visibility-observer/get-scroll-parent.js +45 -0
  137. package/src/components/visibility-observer/visibility-observer.js +109 -0
  138. package/src/components/visibility-observer/visibility-observer.jsx +123 -0
  139. package/src/components/zoomable-tex.jsx +70 -0
  140. package/src/components/zoomable.jsx +304 -0
  141. package/src/components.js +17 -0
  142. package/src/dependencies.js +22 -0
  143. package/src/error-boundary.jsx +48 -0
  144. package/src/extra-widgets.js +75 -0
  145. package/src/gorgon/README +40 -0
  146. package/src/gorgon/__tests__/matcher_test.js +497 -0
  147. package/src/gorgon/__tests__/rule_test.js +101 -0
  148. package/src/gorgon/__tests__/rules_test.js +487 -0
  149. package/src/gorgon/__tests__/selector-parser_test.js +52 -0
  150. package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
  151. package/src/gorgon/gorgon.js +290 -0
  152. package/src/gorgon/proptypes.js +29 -0
  153. package/src/gorgon/rule.js +413 -0
  154. package/src/gorgon/rules/absolute-url.js +24 -0
  155. package/src/gorgon/rules/all-rules.js +72 -0
  156. package/src/gorgon/rules/blockquoted-math.js +10 -0
  157. package/src/gorgon/rules/blockquoted-widget.js +10 -0
  158. package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
  159. package/src/gorgon/rules/extra-content-spacing.js +12 -0
  160. package/src/gorgon/rules/heading-level-1.js +14 -0
  161. package/src/gorgon/rules/heading-level-skip.js +20 -0
  162. package/src/gorgon/rules/heading-sentence-case.js +11 -0
  163. package/src/gorgon/rules/heading-title-case.js +63 -0
  164. package/src/gorgon/rules/image-alt-text.js +21 -0
  165. package/src/gorgon/rules/image-in-table.js +10 -0
  166. package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
  167. package/src/gorgon/rules/image-widget.js +50 -0
  168. package/src/gorgon/rules/link-click-here.js +11 -0
  169. package/src/gorgon/rules/lint-utils.js +48 -0
  170. package/src/gorgon/rules/long-paragraph.js +14 -0
  171. package/src/gorgon/rules/math-adjacent.js +10 -0
  172. package/src/gorgon/rules/math-align-extra-break.js +11 -0
  173. package/src/gorgon/rules/math-align-linebreaks.js +43 -0
  174. package/src/gorgon/rules/math-empty.js +10 -0
  175. package/src/gorgon/rules/math-font-size.js +12 -0
  176. package/src/gorgon/rules/math-frac.js +10 -0
  177. package/src/gorgon/rules/math-nested.js +11 -0
  178. package/src/gorgon/rules/math-starts-with-space.js +12 -0
  179. package/src/gorgon/rules/math-text-empty.js +10 -0
  180. package/src/gorgon/rules/math-without-dollars.js +14 -0
  181. package/src/gorgon/rules/nested-lists.js +11 -0
  182. package/src/gorgon/rules/profanity.js +10 -0
  183. package/src/gorgon/rules/table-missing-cells.js +20 -0
  184. package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
  185. package/src/gorgon/rules/unescaped-dollar.js +10 -0
  186. package/src/gorgon/rules/widget-in-table.js +10 -0
  187. package/src/gorgon/selector.js +506 -0
  188. package/src/gorgon/tree-transformer.js +588 -0
  189. package/src/hint-renderer.jsx +159 -0
  190. package/src/hints-renderer.jsx +359 -0
  191. package/src/icon-paths.js +120 -0
  192. package/src/index.js +149 -2
  193. package/src/init.js +49 -0
  194. package/src/interaction-tracker.js +60 -0
  195. package/src/interactive2/__tests__/movable-mock.js +48 -0
  196. package/src/interactive2/__tests__/movable-point_test.js +146 -0
  197. package/src/interactive2/interactive-util.js +152 -0
  198. package/src/interactive2/movable-helper-methods.js +160 -0
  199. package/src/interactive2/movable-line-options.js +392 -0
  200. package/src/interactive2/movable-line.js +322 -0
  201. package/src/interactive2/movable-point-options.js +190 -0
  202. package/src/interactive2/movable-point.jsx +604 -0
  203. package/src/interactive2/movable-polygon-options.js +351 -0
  204. package/src/interactive2/movable-polygon.js +367 -0
  205. package/src/interactive2/movable.js +273 -0
  206. package/src/interactive2/objective_.js +79 -0
  207. package/src/interactive2/types.js +3 -0
  208. package/src/interactive2/wrapped-defaults.js +86 -0
  209. package/src/interactive2/wrapped-ellipse.js +87 -0
  210. package/src/interactive2/wrapped-line.js +109 -0
  211. package/src/interactive2/wrapped-path.js +47 -0
  212. package/src/interactive2.js +91 -0
  213. package/src/item-renderer.jsx +518 -0
  214. package/src/item-version.js +12 -0
  215. package/src/jipt-paragraphs.js +44 -0
  216. package/src/jquery.mobile.vmouse.js +545 -0
  217. package/src/keypad-context.js +33 -0
  218. package/src/loading-context.js +20 -0
  219. package/src/logging/log.js +107 -0
  220. package/src/logging/log_test.js +41 -0
  221. package/src/mixins/changeable.jsx +115 -0
  222. package/src/mixins/editor-jsonify.jsx +13 -0
  223. package/src/mixins/provide-keypad.jsx +131 -0
  224. package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
  225. package/src/mixins/widget-prop-blacklist.js +36 -0
  226. package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
  227. package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
  228. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
  229. package/src/multi-items/__tests__/items_test.js +245 -0
  230. package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
  231. package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
  232. package/src/multi-items/__tests__/shapes_test.js +44 -0
  233. package/src/multi-items/__tests__/trees_test.js +316 -0
  234. package/src/multi-items/item-types.js +55 -0
  235. package/src/multi-items/items.js +176 -0
  236. package/src/multi-items/multi-renderer.jsx +542 -0
  237. package/src/multi-items/prop-type-builders.js +105 -0
  238. package/src/multi-items/shape-types.js +47 -0
  239. package/src/multi-items/shapes.js +53 -0
  240. package/src/multi-items/tree-types.js +42 -0
  241. package/src/multi-items/trees.js +345 -0
  242. package/src/multi-items.js +76 -0
  243. package/src/not-gorgon.js +129 -0
  244. package/src/perseus-api.jsx +270 -0
  245. package/src/perseus-env.js +26 -0
  246. package/src/perseus-error.js +19 -0
  247. package/src/perseus-markdown.jsx +434 -0
  248. package/src/perseus-types.js +1597 -0
  249. package/src/pure-markdown.js +346 -0
  250. package/src/question-paragraph.jsx +30 -0
  251. package/src/renderability.jsx +108 -0
  252. package/src/renderer.jsx +1931 -0
  253. package/src/server-item-renderer.jsx +467 -0
  254. package/src/sigfigs.jsx +492 -0
  255. package/src/styles/articles.less +748 -0
  256. package/src/styles/constants.js +119 -0
  257. package/src/styles/global-constants.js +350 -0
  258. package/src/styles/global-styles.js +307 -0
  259. package/src/styles/khan-exercise.css +1304 -0
  260. package/src/styles/media-queries.js +48 -0
  261. package/src/styles/perseus-renderer.less +1008 -0
  262. package/src/styles/shared.js +132 -0
  263. package/src/styles/util.less +47 -0
  264. package/src/styles/variables.boostrap.less +301 -0
  265. package/src/styles/variables.less +68 -0
  266. package/src/styles/variables.shared.less +356 -0
  267. package/src/styles/widgets/categorizer.less +39 -0
  268. package/src/styles/widgets/definition.less +86 -0
  269. package/src/styles/widgets/dropdown.less +3 -0
  270. package/src/styles/widgets/expression.less +147 -0
  271. package/src/styles/widgets/graded-group.less +36 -0
  272. package/src/styles/widgets/grapher.less +11 -0
  273. package/src/styles/widgets/group.less +19 -0
  274. package/src/styles/widgets/image.less +29 -0
  275. package/src/styles/widgets/interactive-graph.less +61 -0
  276. package/src/styles/widgets/label-image.less +41 -0
  277. package/src/styles/widgets/matcher.less +8 -0
  278. package/src/styles/widgets/matrix.less +120 -0
  279. package/src/styles/widgets/measurer.less +20 -0
  280. package/src/styles/widgets/orderer.less +97 -0
  281. package/src/styles/widgets/passage.less +151 -0
  282. package/src/styles/widgets/plotter.less +33 -0
  283. package/src/styles/widgets/radio.less +52 -0
  284. package/src/styles/widgets/reaction-diagram.less +35 -0
  285. package/src/styles/widgets/sequence.less +5 -0
  286. package/src/styles/widgets/sortable.less +127 -0
  287. package/src/styles/widgets/table.less +56 -0
  288. package/src/styles/widgets/transformer.less +28 -0
  289. package/src/styles/widgets/unit.less +14 -0
  290. package/src/tex-wrangler.js +176 -0
  291. package/src/traversal.jsx +141 -0
  292. package/src/types.js +538 -0
  293. package/src/util/a11y.js +24 -0
  294. package/src/util/answer-types.js +862 -0
  295. package/src/util/answer-types_test.js +115 -0
  296. package/src/util/colors.js +94 -0
  297. package/src/util/fix-passage-refs.jsx +122 -0
  298. package/src/util/graph-utils.js +16 -0
  299. package/src/util/graphie.js +1585 -0
  300. package/src/util/interactive.js +4071 -0
  301. package/src/util/katex-preprocess.js +16 -0
  302. package/src/util/math.js +296 -0
  303. package/src/util/mobile-native-utils.js +14 -0
  304. package/src/util/mobile-native-utils_test.js +24 -0
  305. package/src/util/register-all-widgets-for-testing.js +12 -0
  306. package/src/util/scroll-utils.js +162 -0
  307. package/src/util/sizing-utils.js +46 -0
  308. package/src/util/tex.js +203 -0
  309. package/src/util/url-utils.js +36 -0
  310. package/src/util.js +990 -0
  311. package/src/version.js +11 -0
  312. package/src/widget-container.jsx +174 -0
  313. package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
  314. package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
  315. package/src/widgets/__stories__/definition.stories.jsx +38 -0
  316. package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
  317. package/src/widgets/__stories__/explanation.stories.jsx +19 -0
  318. package/src/widgets/__stories__/expression.stories.jsx +26 -0
  319. package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
  320. package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
  321. package/src/widgets/__stories__/grapher.stories.jsx +47 -0
  322. package/src/widgets/__stories__/group.stories.jsx +15 -0
  323. package/src/widgets/__stories__/iframe.stories.jsx +15 -0
  324. package/src/widgets/__stories__/image.stories.jsx +62 -0
  325. package/src/widgets/__stories__/input-number.stories.jsx +90 -0
  326. package/src/widgets/__stories__/interaction.stories.jsx +15 -0
  327. package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
  328. package/src/widgets/__stories__/matcher.stories.jsx +15 -0
  329. package/src/widgets/__stories__/matrix.stories.jsx +15 -0
  330. package/src/widgets/__stories__/number-line.stories.jsx +15 -0
  331. package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
  332. package/src/widgets/__stories__/orderer.stories.jsx +15 -0
  333. package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
  334. package/src/widgets/__stories__/passage.stories.jsx +19 -0
  335. package/src/widgets/__stories__/radio.stories.jsx +82 -0
  336. package/src/widgets/__stories__/sorter.stories.jsx +15 -0
  337. package/src/widgets/__stories__/transformer.stories.jsx +15 -0
  338. package/src/widgets/__stories__/video.stories.jsx +15 -0
  339. package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
  340. package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
  341. package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
  342. package/src/widgets/__testdata__/explanation_testdata.js +56 -0
  343. package/src/widgets/__testdata__/expression_testdata.js +95 -0
  344. package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
  345. package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
  346. package/src/widgets/__testdata__/grapher_testdata.js +376 -0
  347. package/src/widgets/__testdata__/group_testdata.js +156 -0
  348. package/src/widgets/__testdata__/iframe_testdata.js +36 -0
  349. package/src/widgets/__testdata__/image_testdata.js +38 -0
  350. package/src/widgets/__testdata__/input-number_testdata.js +73 -0
  351. package/src/widgets/__testdata__/interaction_testdata.js +264 -0
  352. package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
  353. package/src/widgets/__testdata__/matcher_testdata.js +34 -0
  354. package/src/widgets/__testdata__/matrix_testdata.js +29 -0
  355. package/src/widgets/__testdata__/number-line_testdata.js +31 -0
  356. package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
  357. package/src/widgets/__testdata__/orderer_testdata.js +58 -0
  358. package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
  359. package/src/widgets/__testdata__/passage_testdata.js +71 -0
  360. package/src/widgets/__testdata__/radio_testdata.js +210 -0
  361. package/src/widgets/__testdata__/sorter_testdata.js +20 -0
  362. package/src/widgets/__testdata__/transformer_testdata.js +109 -0
  363. package/src/widgets/__testdata__/video_testdata.js +22 -0
  364. package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
  365. package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
  366. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
  367. package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
  368. package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
  369. package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
  370. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
  371. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
  372. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
  373. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
  374. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
  375. package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
  376. package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
  377. package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
  378. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
  379. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
  380. package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
  381. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
  382. package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
  383. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
  384. package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
  385. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
  386. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
  387. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
  388. package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
  389. package/src/widgets/__tests__/categorizer_test.jsx +205 -0
  390. package/src/widgets/__tests__/cs-program_test.jsx +45 -0
  391. package/src/widgets/__tests__/definition_test.js +152 -0
  392. package/src/widgets/__tests__/dropdown_test.jsx +106 -0
  393. package/src/widgets/__tests__/explanation_test.js +199 -0
  394. package/src/widgets/__tests__/expression_test.jsx +439 -0
  395. package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
  396. package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
  397. package/src/widgets/__tests__/graded-group_test.jsx +233 -0
  398. package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
  399. package/src/widgets/__tests__/grapher_test.jsx +35 -0
  400. package/src/widgets/__tests__/group_test.jsx +512 -0
  401. package/src/widgets/__tests__/iframe_test.jsx +47 -0
  402. package/src/widgets/__tests__/image_test.jsx +65 -0
  403. package/src/widgets/__tests__/input-number_test.jsx +360 -0
  404. package/src/widgets/__tests__/interaction_test.jsx +33 -0
  405. package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
  406. package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
  407. package/src/widgets/__tests__/matcher_test.jsx +157 -0
  408. package/src/widgets/__tests__/matrix_test.jsx +84 -0
  409. package/src/widgets/__tests__/number-line_test.jsx +81 -0
  410. package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
  411. package/src/widgets/__tests__/orderer_test.jsx +75 -0
  412. package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
  413. package/src/widgets/__tests__/passage_test.jsx +133 -0
  414. package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
  415. package/src/widgets/__tests__/radio_test.jsx +651 -0
  416. package/src/widgets/__tests__/renderQuestion.jsx +65 -0
  417. package/src/widgets/__tests__/unit_test.jsx +144 -0
  418. package/src/widgets/__tests__/video_test.jsx +55 -0
  419. package/src/widgets/categorizer.jsx +338 -0
  420. package/src/widgets/cs-program.jsx +228 -0
  421. package/src/widgets/definition.jsx +141 -0
  422. package/src/widgets/dropdown.jsx +143 -0
  423. package/src/widgets/explanation.jsx +315 -0
  424. package/src/widgets/expression.jsx +654 -0
  425. package/src/widgets/graded-group-answer-bar.jsx +152 -0
  426. package/src/widgets/graded-group-set.jsx +285 -0
  427. package/src/widgets/graded-group.jsx +448 -0
  428. package/src/widgets/grapher/util.jsx +819 -0
  429. package/src/widgets/grapher.jsx +658 -0
  430. package/src/widgets/group.jsx +213 -0
  431. package/src/widgets/iframe.jsx +199 -0
  432. package/src/widgets/image.jsx +309 -0
  433. package/src/widgets/input-number.jsx +410 -0
  434. package/src/widgets/interaction/arrow-picker.jsx +39 -0
  435. package/src/widgets/interaction/color-picker.jsx +69 -0
  436. package/src/widgets/interaction/dash-picker.jsx +46 -0
  437. package/src/widgets/interaction/element-container.jsx +97 -0
  438. package/src/widgets/interaction.jsx +742 -0
  439. package/src/widgets/interactive-graph.jsx +2821 -0
  440. package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
  441. package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
  442. package/src/widgets/label-image/answer-choices.jsx +311 -0
  443. package/src/widgets/label-image/marker.jsx +365 -0
  444. package/src/widgets/label-image/types.js +33 -0
  445. package/src/widgets/label-image.jsx +841 -0
  446. package/src/widgets/label-image_test.js +840 -0
  447. package/src/widgets/lights-puzzle.jsx +356 -0
  448. package/src/widgets/matcher.jsx +271 -0
  449. package/src/widgets/matrix.jsx +615 -0
  450. package/src/widgets/measurer.jsx +215 -0
  451. package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
  452. package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
  453. package/src/widgets/molecule/molecule-drawing.jsx +175 -0
  454. package/src/widgets/molecule/molecule-layout.jsx +367 -0
  455. package/src/widgets/molecule/smiles-parser.jsx +300 -0
  456. package/src/widgets/molecule.jsx +172 -0
  457. package/src/widgets/number-line.jsx +858 -0
  458. package/src/widgets/numeric-input.jsx +622 -0
  459. package/src/widgets/orderer.jsx +753 -0
  460. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
  461. package/src/widgets/passage/get-line-height-for-node.js +10 -0
  462. package/src/widgets/passage/passage-markdown.jsx +416 -0
  463. package/src/widgets/passage-ref-target.jsx +70 -0
  464. package/src/widgets/passage-ref.jsx +198 -0
  465. package/src/widgets/passage.jsx +601 -0
  466. package/src/widgets/plotter.jsx +1198 -0
  467. package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
  468. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
  469. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
  470. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
  471. package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
  472. package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
  473. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
  474. package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
  475. package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
  476. package/src/widgets/radio/base-radio.jsx +642 -0
  477. package/src/widgets/radio/choice-icon.jsx +468 -0
  478. package/src/widgets/radio/choice-none-above.jsx +54 -0
  479. package/src/widgets/radio/choice.jsx +692 -0
  480. package/src/widgets/radio/choice.jsx.fixture.js +64 -0
  481. package/src/widgets/radio/cross-out-button.jsx +136 -0
  482. package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
  483. package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
  484. package/src/widgets/radio/focus-ring.jsx +51 -0
  485. package/src/widgets/radio/option-status.jsx +89 -0
  486. package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
  487. package/src/widgets/radio/util.js +32 -0
  488. package/src/widgets/radio/util_test.js +19 -0
  489. package/src/widgets/radio/widget.jsx +470 -0
  490. package/src/widgets/radio.jsx +156 -0
  491. package/src/widgets/reaction-diagram.jsx +136 -0
  492. package/src/widgets/sequence.jsx +140 -0
  493. package/src/widgets/simulator.jsx +808 -0
  494. package/src/widgets/sorter.jsx +146 -0
  495. package/src/widgets/table.jsx +395 -0
  496. package/src/widgets/transformer.jsx +2870 -0
  497. package/src/widgets/unit.jsx +371 -0
  498. package/src/widgets/video-transcript-link.jsx +84 -0
  499. package/src/widgets/video-transcript-link_test.js +211 -0
  500. package/src/widgets/video.jsx +131 -0
  501. package/src/widgets.js +431 -0
  502. package/src/zoom.js +539 -0
  503. package/src/__tests__/index.test.js +0 -14
@@ -0,0 +1,642 @@
1
+ // @flow
2
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
3
+ import {StyleSheet, css} from "aphrodite";
4
+ import classNames from "classnames";
5
+ import PropTypes from "prop-types";
6
+ import * as React from "react";
7
+ import ReactDOM from "react-dom";
8
+ import _ from "underscore";
9
+
10
+ import {Errors as PerseusErrors, Log} from "../../logging/log.js";
11
+ import {ClassNames as ApiClassNames} from "../../perseus-api.jsx";
12
+ import * as styleConstants from "../../styles/constants.js";
13
+ import mediaQueries from "../../styles/media-queries.js";
14
+ import sharedStyles from "../../styles/shared.js";
15
+ import Util from "../../util.js";
16
+ import {scrollElementIntoView} from "../../util/scroll-utils.js";
17
+
18
+ import ChoiceNoneAbove from "./choice-none-above.jsx";
19
+ import Choice from "./choice.jsx";
20
+
21
+ import type {StyleDeclaration} from "aphrodite";
22
+
23
+ const {captureScratchpadTouchStart} = Util;
24
+
25
+ const ChoicesType = PropTypes.arrayOf(
26
+ PropTypes.shape({
27
+ // Indicates whether this choice is checked.
28
+ checked: PropTypes.bool,
29
+
30
+ // Indicates whether the user has "crossed out" this choice, meaning
31
+ // that they don't think it's correct. This value does not affect
32
+ // scoring or other behavior; it's just a note for the user's
33
+ // reference.
34
+ crossedOut: PropTypes.bool,
35
+
36
+ content: PropTypes.node,
37
+ rationale: PropTypes.node,
38
+ hasRationale: PropTypes.bool,
39
+ showRationale: PropTypes.bool,
40
+ showCorrectness: PropTypes.bool,
41
+ correct: PropTypes.bool,
42
+ originalIndex: PropTypes.number,
43
+ isNoneOfTheAbove: PropTypes.bool,
44
+ }),
45
+ );
46
+
47
+ const radioBorderColor = styleConstants.radioBorderColor;
48
+
49
+ class BaseRadio extends React.Component<$FlowFixMe, $FlowFixMe> {
50
+ static propTypes = {
51
+ apiOptions: PropTypes.shape({
52
+ readOnly: PropTypes.bool,
53
+ satStyling: PropTypes.bool,
54
+ isMobile: PropTypes.bool,
55
+ styling: PropTypes.shape({
56
+ radioStyleVersion: PropTypes.oneOf(["intermediate", "final"]),
57
+ }),
58
+ canScrollPage: PropTypes.bool,
59
+ }),
60
+ choices: ChoicesType,
61
+ deselectEnabled: PropTypes.bool,
62
+ editMode: PropTypes.bool,
63
+ labelWrap: PropTypes.bool,
64
+ countChoices: PropTypes.bool,
65
+ numCorrect: PropTypes.number,
66
+ multipleSelect: PropTypes.bool,
67
+ reviewModeRubric: PropTypes.shape({
68
+ choices: ChoicesType,
69
+ }),
70
+
71
+ // A callback indicating that this choice has changed. Its argument is
72
+ // an object with two keys: `checked` and `crossedOut`. Each contains
73
+ // an array of boolean values, specifying the new checked and
74
+ // crossed-out value of each choice.
75
+ onChange: PropTypes.func,
76
+
77
+ // Whether this widget was the most recently used widget in this
78
+ // Renderer. Determines whether we'll auto-scroll the page upon
79
+ // entering review mode.
80
+ isLastUsedWidget: PropTypes.bool,
81
+ };
82
+
83
+ static styles: StyleDeclaration = StyleSheet.create({
84
+ // eslint-disable-next-line react-native/no-unused-styles
85
+ instructions: {
86
+ display: "block",
87
+ color: styleConstants.gray17,
88
+ fontSize: 14,
89
+ lineHeight: 1.25,
90
+ fontStyle: "normal",
91
+ fontWeight: "bold",
92
+ marginBottom: 16,
93
+ },
94
+
95
+ // eslint-disable-next-line react-native/no-unused-styles
96
+ instructionsMobile: {
97
+ fontSize: 18,
98
+ [mediaQueries.smOrSmaller]: {
99
+ fontSize: 16,
100
+ },
101
+ // TODO(emily): We want this to match choice text, which turns
102
+ // to 20px at min-width 1200px, but this media query is
103
+ // min-width 1280px because our media queries don't exactly
104
+ // match pure. Make those match up.
105
+ [mediaQueries.xl]: {
106
+ fontSize: 20,
107
+ },
108
+ },
109
+
110
+ // eslint-disable-next-line react-native/no-unused-styles
111
+ radio: {
112
+ // Avoid centering
113
+ width: "100%",
114
+ },
115
+
116
+ // eslint-disable-next-line react-native/no-unused-styles
117
+ responsiveRadioContainer: {
118
+ borderBottom: `1px solid ${radioBorderColor}`,
119
+ borderTop: `1px solid ${radioBorderColor}`,
120
+ width: "auto",
121
+ [mediaQueries.smOrSmaller]: {
122
+ marginLeft: styleConstants.negativePhoneMargin,
123
+ marginRight: styleConstants.negativePhoneMargin,
124
+ },
125
+ },
126
+
127
+ // eslint-disable-next-line react-native/no-unused-styles
128
+ radioContainerFirstHighlighted: {
129
+ borderTop: `1px solid rgba(0, 0, 0, 0)`,
130
+ },
131
+
132
+ // eslint-disable-next-line react-native/no-unused-styles
133
+ radioContainerLastHighlighted: {
134
+ borderBottom: `1px solid rgba(0, 0, 0, 0)`,
135
+ },
136
+
137
+ // eslint-disable-next-line react-native/no-unused-styles
138
+ satRadio: {
139
+ background: "none",
140
+ marginLeft: 0,
141
+ userSelect: "none",
142
+ },
143
+
144
+ // eslint-disable-next-line react-native/no-unused-styles
145
+ satRadioOption: {
146
+ margin: 0,
147
+ padding: 0,
148
+ borderBottom: `1px solid #ccc`,
149
+ ":first-child": {
150
+ borderTop: `1px solid #ccc`,
151
+ },
152
+ },
153
+
154
+ // eslint-disable-next-line react-native/no-unused-styles
155
+ satRadioOptionCorrect: {
156
+ borderBottomColor: styleConstants.satCorrectBorderColor,
157
+ ":first-child": {
158
+ borderTopColor: styleConstants.satCorrectBorderColor,
159
+ },
160
+ },
161
+
162
+ // eslint-disable-next-line react-native/no-unused-styles
163
+ satRadioOptionIncorrect: {
164
+ borderBottomColor: styleConstants.satIncorrectBorderColor,
165
+ ":first-child": {
166
+ borderTopColor: styleConstants.satIncorrectBorderColor,
167
+ },
168
+ },
169
+
170
+ // eslint-disable-next-line react-native/no-unused-styles
171
+ satRadioOptionNextCorrect: {
172
+ borderBottomColor: styleConstants.satCorrectBorderColor,
173
+ },
174
+
175
+ // eslint-disable-next-line react-native/no-unused-styles
176
+ satRadioOptionNextIncorrect: {
177
+ borderBottomColor: styleConstants.satIncorrectBorderColor,
178
+ },
179
+
180
+ // eslint-disable-next-line react-native/no-unused-styles
181
+ satReviewRadioOption: {
182
+ pointerEvents: "none",
183
+ },
184
+
185
+ // eslint-disable-next-line react-native/no-unused-styles
186
+ item: {
187
+ marginLeft: 20,
188
+ },
189
+
190
+ // eslint-disable-next-line react-native/no-unused-styles
191
+ inlineItem: {
192
+ display: "inline-block",
193
+ paddingLeft: 20,
194
+ verticalAlign: "middle",
195
+ // See http://stackoverflow.com/q/8120466 for explanation of
196
+ // why vertical align property is needed
197
+ },
198
+
199
+ // eslint-disable-next-line react-native/no-unused-styles
200
+ responsiveItem: {
201
+ marginLeft: 0,
202
+ padding: 0,
203
+
204
+ ":not(:last-child)": {
205
+ borderBottom: `1px solid ${radioBorderColor}`,
206
+ },
207
+ },
208
+
209
+ // eslint-disable-next-line react-native/no-unused-styles
210
+ selectedItem: {
211
+ background: "white",
212
+ },
213
+
214
+ // eslint-disable-next-line react-native/no-unused-styles
215
+ aboveBackdrop: {
216
+ position: "relative",
217
+ // HACK(emily): We want selected choices to show up above our
218
+ // exercise backdrop, but below the exercise footer and
219
+ // "feedback popover" that shows up. This z-index is carefully
220
+ // coordinated between here and webapp. :(
221
+ zIndex: 1062,
222
+ },
223
+
224
+ // eslint-disable-next-line react-native/no-unused-styles
225
+ aboveBackdropMobile: {
226
+ boxShadow:
227
+ "0 0 4px 0 rgba(0, 0, 0, 0.2)," +
228
+ "0 0 2px 0 rgba(0, 0, 0, 0.1)",
229
+
230
+ ":not(:last-child)": {
231
+ borderBottom: `1px solid rgba(0, 0, 0, 0)`,
232
+ },
233
+ },
234
+
235
+ // eslint-disable-next-line react-native/no-unused-styles
236
+ nextHighlighted: {
237
+ ":not(:last-child)": {
238
+ borderBottom: `1px solid rgba(0, 0, 0, 0)`,
239
+ },
240
+ },
241
+
242
+ // eslint-disable-next-line react-native/no-unused-styles
243
+ responsiveContainer: {
244
+ overflow: "auto",
245
+ marginLeft: styleConstants.negativePhoneMargin,
246
+ marginRight: styleConstants.negativePhoneMargin,
247
+ paddingLeft: styleConstants.phoneMargin,
248
+ // paddingRight is handled by responsiveFieldset
249
+ },
250
+
251
+ // eslint-disable-next-line react-native/no-unused-styles
252
+ responsiveFieldset: {
253
+ paddingRight: styleConstants.phoneMargin,
254
+ },
255
+ });
256
+
257
+ static defaultProps: $FlowFixMe = {
258
+ editMode: false,
259
+ };
260
+
261
+ state: $FlowFixMe = {
262
+ // TODO(mdr): This keeps the ID stable across re-renders on the
263
+ // same machine, but, at time of writing, the server's state
264
+ // isn't rehydrated to the client during SSR, so the server and
265
+ // client will generate different IDs and cause a mismatch
266
+ // during SSR :(
267
+ radioGroupName: _.uniqueId("perseus_radio_"),
268
+ };
269
+
270
+ componentDidUpdate(prevProps: $FlowFixMe) {
271
+ const {apiOptions, choices, isLastUsedWidget, reviewModeRubric} =
272
+ this.props;
273
+
274
+ // Switching into review mode can sometimes cause the selected answer
275
+ // to scroll out of view - for example, when we reveal all those tall
276
+ // rationales. This can be disorienting for the user.
277
+ //
278
+ // So, if this widget just switched into review mode, ensure that the
279
+ // checked choice (or first checked choice, if there are multiple) is
280
+ // scrolled into view.
281
+ //
282
+ // This only happens if the `canScrollPage` API option is set (so that
283
+ // call sites aren't surprised by scrolling), and if this widget was
284
+ // the most recently used widget (because, if the user has since
285
+ // touched something else, they're probably not trying to keep their
286
+ // eye on this widget anymore).
287
+ if (
288
+ apiOptions.canScrollPage &&
289
+ isLastUsedWidget &&
290
+ reviewModeRubric &&
291
+ !prevProps.reviewModeRubric
292
+ ) {
293
+ const checkedIndex = choices.findIndex((c) => c.checked);
294
+ if (checkedIndex >= 0) {
295
+ const checkedNode: $FlowFixMe = ReactDOM.findDOMNode(
296
+ // eslint-disable-next-line react/no-string-refs
297
+ this.refs["radio" + checkedIndex],
298
+ );
299
+ scrollElementIntoView(checkedNode);
300
+ }
301
+ }
302
+ }
303
+
304
+ // When a particular choice's `onChange` handler is called, indicating a
305
+ // change in a single choice's values, we need to call our `onChange`
306
+ // handler in order to notify our parent. However, our API with our parent
307
+ // is that we always provide *all* values for *all* choices, even if just
308
+ // one choice's values changed. (This is because sometimes an interaction
309
+ // with one choice can affect many choices, like how checking a new answer
310
+ // will usually cause the old answer to become unchecked.)
311
+ //
312
+ // So, given the new values for a particular choice, compute the new values
313
+ // for all choices, and pass them to `this.props.onChange`.
314
+ //
315
+ // `newValues` is an object with two keys: `checked` and `crossedOut`. Each
316
+ // contains a boolean value specifying the new checked and crossed-out
317
+ // value of this choice.
318
+ updateChoice: (number, $FlowFixMe) => void = (choiceIndex, newValues) => {
319
+ // Get the baseline `checked` values. If we're checking a new answer
320
+ // and multiple-select is not on, we should clear all choices to be
321
+ // unchecked. Otherwise, we should copy the old checked values.
322
+ let newCheckedList;
323
+ if (newValues.checked && !this.props.multipleSelect) {
324
+ newCheckedList = this.props.choices.map((_) => false);
325
+ } else {
326
+ newCheckedList = this.props.choices.map((c) => c.checked);
327
+ }
328
+
329
+ // Get the baseline `crossedOut` values.
330
+ const newCrossedOutList = this.props.choices.map((c) => c.crossedOut);
331
+
332
+ // Update this choice's `checked` and `crossedOut` values.
333
+ newCheckedList[choiceIndex] = newValues.checked;
334
+ newCrossedOutList[choiceIndex] = newValues.crossedOut;
335
+
336
+ this.props.onChange({
337
+ checked: newCheckedList,
338
+ crossedOut: newCrossedOutList,
339
+ });
340
+ };
341
+
342
+ focus: (number) => boolean = (i) => {
343
+ // $FlowFixMe[incompatible-use]
344
+ // $FlowFixMe[prop-missing]
345
+ ReactDOM.findDOMNode(this.refs["radio" + (i || 0)]).focus(); // eslint-disable-line react/no-string-refs
346
+ return true;
347
+ };
348
+
349
+ getInstructionsText: () => string = () => {
350
+ if (this.props.multipleSelect) {
351
+ if (this.props.countChoices) {
352
+ return i18n._("Choose %(numCorrect)s answers:", {
353
+ numCorrect: this.props.numCorrect,
354
+ });
355
+ }
356
+ return i18n._("Choose all answers that apply:");
357
+ }
358
+ return i18n._("Choose 1 answer:");
359
+ };
360
+
361
+ deselectEnabled: () => boolean = () => {
362
+ // We want to force enable deselect on mobile.
363
+ return this.props.apiOptions.isMobile || this.props.deselectEnabled;
364
+ };
365
+
366
+ /**
367
+ * Find the choice at the given index (wrapped, if necessary), focus it,
368
+ * uncheck the old choice, and check the new choice (unless it's crossed
369
+ * out, in which case we only uncheck the old choice).
370
+ *
371
+ * This is very similar to standard up/down arrow behavior, except that we
372
+ * don't select crossed-out choices.
373
+ *
374
+ * Handles `goToPrevChoice` and `goToNextChoice` calls from our children.
375
+ */
376
+ goToChoice: (number) => void = (newChoiceIndex) => {
377
+ const numChoices = this.props.choices.length;
378
+
379
+ // Wrap the newChoiceIndex around the start/end, if necessary.
380
+ if (newChoiceIndex < 0) {
381
+ newChoiceIndex += numChoices;
382
+ }
383
+ if (newChoiceIndex >= numChoices) {
384
+ newChoiceIndex -= numChoices;
385
+ }
386
+
387
+ // Focus the new choice's input element.
388
+ // eslint-disable-next-line react/no-string-refs
389
+ const choiceInstance = this.refs[`radio${newChoiceIndex}`];
390
+ if (!choiceInstance) {
391
+ Log.error(
392
+ `found no choice at index ${newChoiceIndex}, even after ` +
393
+ `wrapping it to be within bounds`,
394
+ PerseusErrors.Internal,
395
+ {loggedMetadata: {choices: JSON.stringify(this.props.choices)}},
396
+ );
397
+ return;
398
+ }
399
+ choiceInstance.focusInput();
400
+
401
+ // We aren't going to change any choices' crossed-out state, but we
402
+ // _are_ going to deselect all choices.
403
+ const newCrossedOutList = this.props.choices.map((c) => c.crossedOut);
404
+ const newCheckedList = this.props.choices.map((c) => false);
405
+
406
+ // And, if the new choice isn't crossed out, we'll select it.
407
+ if (!this.props.choices[newChoiceIndex].crossedOut) {
408
+ newCheckedList[newChoiceIndex] = true;
409
+ }
410
+
411
+ this.props.onChange({
412
+ checked: newCheckedList,
413
+ crossedOut: newCrossedOutList,
414
+ });
415
+ };
416
+
417
+ render(): React.Node {
418
+ const inputType = this.props.multipleSelect ? "checkbox" : "radio";
419
+ const rubric = this.props.reviewModeRubric;
420
+ const reviewMode = !!rubric;
421
+
422
+ const styles = BaseRadio.styles;
423
+ const sat = this.props.apiOptions.satStyling;
424
+
425
+ const isMobile = this.props.apiOptions.isMobile;
426
+
427
+ const choices = this.props.choices;
428
+ const firstChoiceHighlighted = choices[0].highlighted;
429
+ const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
430
+
431
+ const className = classNames(
432
+ "perseus-widget-radio",
433
+ !this.props.editMode && "perseus-rendered-radio",
434
+ css(
435
+ styles.radio,
436
+ // SAT doesn't use the "responsive styling" as it conflicts
437
+ // with their custom theming.
438
+ !sat && styles.responsiveRadioContainer,
439
+ !sat &&
440
+ firstChoiceHighlighted &&
441
+ isMobile &&
442
+ styles.radioContainerFirstHighlighted,
443
+ !sat &&
444
+ lastChoiceHighlighted &&
445
+ isMobile &&
446
+ styles.radioContainerLastHighlighted,
447
+ sat && styles.satRadio,
448
+ ),
449
+ );
450
+
451
+ const instructionsClassName = classNames(
452
+ "instructions",
453
+ css(styles.instructions, isMobile && styles.instructionsMobile),
454
+ );
455
+ const instructions = this.getInstructionsText();
456
+ const shouldShowInstructions = !sat;
457
+
458
+ const responsiveClassName = css(styles.responsiveFieldset);
459
+ const fieldset = (
460
+ <fieldset
461
+ className={`perseus-widget-radio-fieldset ${responsiveClassName}`}
462
+ >
463
+ <legend className="perseus-sr-only">{instructions}</legend>
464
+ {shouldShowInstructions && (
465
+ <div className={instructionsClassName}>{instructions}</div>
466
+ )}
467
+ <ul className={className}>
468
+ {this.props.choices.map(function (choice, i) {
469
+ let Element = Choice;
470
+ const elementProps = {
471
+ ref: `radio${i}`,
472
+ apiOptions: this.props.apiOptions,
473
+ checked: choice.checked,
474
+ crossedOut: choice.crossedOut,
475
+ previouslyAnswered: choice.previouslyAnswered,
476
+ reviewMode,
477
+ correct: choice.correct,
478
+ rationale: choice.rationale,
479
+ content: choice.content,
480
+ disabled:
481
+ this.props.apiOptions.readOnly ||
482
+ choice.disabled,
483
+ editMode: this.props.editMode,
484
+ groupName: this.state.radioGroupName,
485
+ isLastChoice: i === this.props.choices.length - 1,
486
+ showCorrectness:
487
+ reviewMode || !!choice.showCorrectness,
488
+ showRationale:
489
+ choice.hasRationale &&
490
+ (reviewMode || choice.showRationale),
491
+ type: inputType,
492
+ pos: i,
493
+ deselectEnabled: this.deselectEnabled(),
494
+ onChange: (newValues) => {
495
+ this.updateChoice(i, newValues);
496
+ },
497
+ goToPrevChoice: () => this.goToChoice(i - 1),
498
+ goToNextChoice: () => this.goToChoice(i + 1),
499
+ };
500
+
501
+ if (choice.isNoneOfTheAbove) {
502
+ Element = ChoiceNoneAbove;
503
+ _.extend(elementProps, {
504
+ showContent: choice.revealNoneOfTheAbove,
505
+ });
506
+ }
507
+
508
+ const nextChoice = this.props.choices[i + 1];
509
+ const nextChoiceHighlighted =
510
+ !!nextChoice && nextChoice.highlighted;
511
+
512
+ const aphroditeClassName = (checked) => {
513
+ // Whether or not to show correctness borders
514
+ // for this choice and the next choice.
515
+ const satShowCorrectness =
516
+ sat && reviewMode && checked;
517
+ const satShowCorrectnessNext =
518
+ sat &&
519
+ reviewMode &&
520
+ nextChoice &&
521
+ nextChoice.checked;
522
+
523
+ return css(
524
+ sharedStyles.aboveScratchpad,
525
+ styles.item,
526
+ !sat && styles.responsiveItem,
527
+ !sat && checked && styles.selectedItem,
528
+ !sat &&
529
+ checked &&
530
+ choice.highlighted &&
531
+ styles.aboveBackdrop,
532
+ !sat &&
533
+ checked &&
534
+ choice.highlighted &&
535
+ this.props.apiOptions.isMobile &&
536
+ styles.aboveBackdropMobile,
537
+ !sat &&
538
+ nextChoiceHighlighted &&
539
+ this.props.apiOptions.isMobile &&
540
+ styles.nextHighlighted,
541
+ sat && styles.satRadioOption,
542
+ satShowCorrectness &&
543
+ !choice.correct &&
544
+ styles.satRadioOptionIncorrect,
545
+ satShowCorrectness &&
546
+ choice.correct &&
547
+ styles.satRadioOptionCorrect,
548
+ satShowCorrectnessNext &&
549
+ !nextChoice.correct &&
550
+ styles.satRadioOptionNextIncorrect,
551
+ satShowCorrectnessNext &&
552
+ nextChoice.correct &&
553
+ styles.satRadioOptionNextCorrect,
554
+ sat && rubric && styles.satReviewRadioOption,
555
+ );
556
+ };
557
+
558
+ // HACK(abdulrahman): Preloads the selection-state
559
+ // css because of a bug that causes iOS to lag
560
+ // when selecting the button for the first time.
561
+ aphroditeClassName(true);
562
+
563
+ const className = classNames(
564
+ aphroditeClassName(choice.checked),
565
+ // TODO(aria): Make test case for these API
566
+ // classNames
567
+ ApiClassNames.RADIO.OPTION,
568
+ choice.checked && ApiClassNames.RADIO.SELECTED,
569
+ reviewMode &&
570
+ rubric.choices[i].correct &&
571
+ ApiClassNames.CORRECT,
572
+ reviewMode &&
573
+ !rubric.choices[i].correct &&
574
+ ApiClassNames.INCORRECT,
575
+ );
576
+
577
+ // In edit mode, the Choice renders a Div in order to
578
+ // allow for the contentEditable area to be selected
579
+ // (label forces any clicks inside to select the input
580
+ // element) We have to add some extra behavior to make
581
+ // sure that we can still check the choice.
582
+ let listElem = null;
583
+ let clickHandler = null;
584
+ if (this.props.editMode) {
585
+ clickHandler = (e) => {
586
+ // Traverse the parent nodes of the clicked
587
+ // element.
588
+ let elem = e.target;
589
+ while (elem && elem !== listElem) {
590
+ // If the clicked element is inside of the
591
+ // radio icon, then we want to trigger the
592
+ // check by flipping the choice of the icon.
593
+ if (
594
+ elem.getAttribute("data-is-radio-icon")
595
+ ) {
596
+ this.updateChoice(i, {
597
+ checked: !choice.checked,
598
+ });
599
+ return;
600
+ }
601
+ elem = elem.parentNode;
602
+ }
603
+ };
604
+ }
605
+
606
+ // TODO(mattdr): Index isn't a *good* choice of key
607
+ // here; is there a better one? Can we use choice
608
+ // content somehow? Would changing our choice of key
609
+ // somehow break something happening inside a choice's
610
+ // child Renderers, by changing when we mount/unmount?
611
+ return (
612
+ <li
613
+ key={i}
614
+ ref={(e) => (listElem = e)}
615
+ className={className}
616
+ onClick={clickHandler}
617
+ onTouchStart={
618
+ !this.props.labelWrap
619
+ ? null
620
+ : captureScratchpadTouchStart
621
+ }
622
+ >
623
+ <Element {...elementProps} />
624
+ </li>
625
+ );
626
+ }, this)}
627
+ </ul>
628
+ </fieldset>
629
+ );
630
+
631
+ // Allow for horizontal scrolling if content is too wide, which may be
632
+ // an issue especially on phones.
633
+ // This is disabled in SAT, since it conflicts with their theming.
634
+ return (
635
+ <div className={css(!sat && styles.responsiveContainer)}>
636
+ {fieldset}
637
+ </div>
638
+ );
639
+ }
640
+ }
641
+
642
+ export default BaseRadio;