@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,64 @@
1
+ // @flow
2
+ export const instances: $ReadOnlyArray<mixed> = [
3
+ ({}: {...}),
4
+ {
5
+ content: "Plain Content",
6
+ goToPrevChoice: () => {},
7
+ goToNextChoice: () => {},
8
+ },
9
+ {
10
+ content: "Correct Content (Review)",
11
+ correct: true,
12
+ reviewMode: true,
13
+ goToPrevChoice: () => {},
14
+ goToNextChoice: () => {},
15
+ },
16
+ {
17
+ content: "Incorrect Content (Review)",
18
+ correct: false,
19
+ reviewMode: true,
20
+ goToPrevChoice: () => {},
21
+ goToNextChoice: () => {},
22
+ },
23
+ {
24
+ content: "Content (Review, correctness not specified)",
25
+ reviewMode: true,
26
+ goToPrevChoice: () => {},
27
+ goToNextChoice: () => {},
28
+ },
29
+ {
30
+ content: "Content with Rationale",
31
+ rationale: "This is some additional information",
32
+ showRationale: true,
33
+ goToPrevChoice: () => {},
34
+ goToNextChoice: () => {},
35
+ },
36
+ {
37
+ content: "Correct Content with Rationale (Review)",
38
+ correct: true,
39
+ rationale: "This is some additional information",
40
+ reviewMode: true,
41
+ showRationale: true,
42
+ goToPrevChoice: () => {},
43
+ goToNextChoice: () => {},
44
+ },
45
+ {
46
+ content: "Incorrect Content with Rationale (Review)",
47
+ correct: false,
48
+ rationale: "This is some additional information",
49
+ reviewMode: true,
50
+ showRationale: true,
51
+ goToPrevChoice: () => {},
52
+ goToNextChoice: () => {},
53
+ },
54
+ {
55
+ content: "Content with Rationale (Review, correctness not specified)",
56
+ rationale: "This is some additional information",
57
+ reviewMode: true,
58
+ showRationale: true,
59
+ goToPrevChoice: () => {},
60
+ goToNextChoice: () => {},
61
+ },
62
+ ];
63
+
64
+ export const log = ["goToPrevChoice", "goToNextChoice"];
@@ -0,0 +1,136 @@
1
+ // @flow
2
+ /**
3
+ * The button that appears when you open the `CrossOutMenu`, enabling the user
4
+ * to toggle a choice's `crossedOut` state.
5
+ */
6
+ import colors from "@khanacademy/wonder-blocks-color";
7
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
8
+ import {LabelLarge} from "@khanacademy/wonder-blocks-typography";
9
+ import {StyleSheet, css} from "aphrodite";
10
+ import * as React from "react";
11
+
12
+ import {colors as oldColors} from "../../styles/global-styles.js";
13
+
14
+ import ChoiceIcon from "./choice-icon.jsx";
15
+ import {getChoiceLetter} from "./util.js";
16
+
17
+ export type CrossOutButtonProps = {
18
+ onClick: () => void,
19
+ onFocus: () => void,
20
+ onBlur: () => void,
21
+ crossedOut: boolean,
22
+ pos: number,
23
+ primaryProductColor?: string,
24
+ disabled?: boolean,
25
+ tabFocusable?: boolean,
26
+ usingKeyboardFocus: boolean,
27
+ // Day theme uses dark colors, for use on light backgrounds (tooltip).
28
+ // Night theme uses light colors, for use on dark backgrounds (slide panel).
29
+ //
30
+ // TODO(mdr): The day/night naming is used in Test Prep, but I've also seen
31
+ // a similar concept in another component (tooltip), as a boolean prop
32
+ // named `inverted`. If one of those emerges as the standard, we should
33
+ // switch to that!
34
+ theme: "day" | "night",
35
+ ...
36
+ };
37
+
38
+ export default class CrossOutButton extends React.PureComponent<CrossOutButtonProps> {
39
+ static defaultProps: {|primaryProductColor: string|} = {
40
+ primaryProductColor: oldColors.kaGreen,
41
+ };
42
+
43
+ /**
44
+ * A ref function for the tooltip button. Unlike most ref functions, where
45
+ * we save/delete the node on mount/unmount, we instead use this handler to
46
+ * auto-focus the button on mount, if appropriate.
47
+ */
48
+ _handleTooltipButtonRef: (tooltipButton: ?HTMLButtonElement) => void = (
49
+ tooltipButton: ?HTMLButtonElement,
50
+ ) => {
51
+ if (tooltipButton && this.props.usingKeyboardFocus) {
52
+ tooltipButton.focus();
53
+ }
54
+ };
55
+
56
+ _getAriaLabel(): string {
57
+ const {crossedOut, pos} = this.props;
58
+ const letter = getChoiceLetter(pos);
59
+
60
+ if (crossedOut) {
61
+ return i18n._("Bring back Choice %(letter)s", {letter});
62
+ }
63
+ return i18n._("Cross out Choice %(letter)s", {letter});
64
+ }
65
+
66
+ render(): React.Element<"button"> {
67
+ const {crossedOut, pos, primaryProductColor, theme} = this.props;
68
+
69
+ const buttonAndTextColor =
70
+ theme === "day" ? primaryProductColor : colors.white;
71
+
72
+ return (
73
+ <button
74
+ type="button"
75
+ aria-label={this._getAriaLabel()}
76
+ className={css([styles.tooltipButton])}
77
+ onClick={() => this.props.onClick()}
78
+ // Don't focus this element on mousedown. It's visually
79
+ // distracting, and confuses our "should we shift focus"
80
+ // logic for keyboard users.
81
+ onMouseDown={(e) => e.preventDefault()}
82
+ onFocus={() => this.props.onFocus()}
83
+ onBlur={() => this.props.onBlur()}
84
+ disabled={this.props.disabled}
85
+ tabIndex={this.props.tabFocusable ? 0 : -1}
86
+ ref={this._handleTooltipButtonRef}
87
+ >
88
+ <ChoiceIcon
89
+ pos={pos}
90
+ product="library"
91
+ // This icon is a preview of the _other_ state, so we use
92
+ // !crossedOut, the opposite of our current state.
93
+ crossedOut={!this.props.crossedOut}
94
+ // HACK(mdr): Here, we're using ChoiceIcon as a visual
95
+ // element: we want a non-filled bubble, with custom
96
+ // colored stroke/text rather than gray stroke/text.
97
+ // Unfortunately for us, ChoiceIcon has semantic props,
98
+ // so we have to hardcode the semantics that give us
99
+ // the visuals we want. This is a bit confusing and a
100
+ // bit fragile, but for now it seems better than adding
101
+ // complexity to the ChoiceIcon API!
102
+ primaryProductColor={buttonAndTextColor}
103
+ pressed={true}
104
+ checked={false}
105
+ focused={false}
106
+ correct={false}
107
+ showCorrectness={false}
108
+ reviewMode={false}
109
+ previouslyAnswered={false}
110
+ transparentBackground={true}
111
+ />
112
+ <LabelLarge style={{color: buttonAndTextColor}}>
113
+ {crossedOut ? i18n._("Bring back") : i18n._("Cross out")}
114
+ </LabelLarge>
115
+ </button>
116
+ );
117
+ }
118
+ }
119
+
120
+ const styles = StyleSheet.create({
121
+ tooltipButton: {
122
+ // Reset <button> styles to be more like <div>.
123
+ background: "none",
124
+ border: "none",
125
+ font: "inherit",
126
+ padding: 0,
127
+
128
+ cursor: "pointer",
129
+ textDecoration: "none",
130
+
131
+ display: "flex",
132
+ flexDirection: "column",
133
+ alignItems: "center",
134
+ textAlign: "center",
135
+ },
136
+ });
@@ -0,0 +1,492 @@
1
+ /* eslint-disable react/no-unsafe */
2
+ // @flow
3
+ /**
4
+ * A wrapper for radio choice UI. This adds an ellipsis menu on its right-hand
5
+ * side that, when clicked, reveals a `CrossOutButton`.
6
+ *
7
+ * On desktop, we reveal this button by showing a simple tooltip.
8
+ *
9
+ * On mobile, we reveal this button by sliding the choice content to the left,
10
+ * to uncover a cross-out button underneath. (This is why we need to wrap the
11
+ * content!)
12
+ */
13
+ import colors from "@khanacademy/wonder-blocks-color";
14
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
15
+ import {StyleSheet, css} from "aphrodite";
16
+ import * as React from "react";
17
+
18
+ import Icon from "../../components/icon.jsx";
19
+ import mediaQueries from "../../styles/media-queries.js";
20
+
21
+ import CrossOutButton from "./cross-out-button.jsx";
22
+ import CrossOutTooltip from "./cross-out-tooltip.jsx";
23
+ import {getChoiceLetter} from "./util.js";
24
+
25
+ // TODO(mdr): The icon in the cross-out spec isn't this one. The spec's icon
26
+ // rounder dots.
27
+ const ellipsisHorizontalIcon = {
28
+ path: "M27.218 6.82l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836zm36.27 0l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836zm36.27 0l0 13.578q0 2.852-1.984 4.836t-4.836 1.984l-13.578 0q-2.852 0-4.836-1.984t-1.984-4.836l0-13.578q0-2.852 1.984-4.836t4.836-1.984l13.578 0q2.852 0 4.836 1.984t1.984 4.836z",
29
+ width: 100,
30
+ height: 27.284,
31
+ };
32
+
33
+ type CrossOutMenuWrapperProps = {
34
+ // The main radio choice UI.
35
+ children: React.Node,
36
+ // Whether to show the cross-out UI at all.
37
+ enabled: boolean,
38
+ // The choice's position in the question, e.g., `0` for choice "A".
39
+ pos: number,
40
+ // The "primary product color" that Perseus has been configured with.
41
+ // Optional; defaults to `kaGreen`.
42
+ primaryProductColor?: string,
43
+ // Whether the choice is currently crossed out.
44
+ crossedOut: boolean,
45
+ // Callback for when the user requests to change the crossed-out state.
46
+ onCrossedOutChange: (crossedOut: boolean) => void,
47
+ // Whether cross out is for SAT tasks.
48
+ isSatProduct?: boolean,
49
+ ...
50
+ };
51
+
52
+ type CrossOutMenuWrapperState = {
53
+ // Whether the menu area is being hovered.
54
+ //
55
+ // This controls the menu icon's color.
56
+ hovered: boolean,
57
+ // Whether the menu is currently open.
58
+ //
59
+ // This controls the tooltip on desktop, and the slide panel on mobile.
60
+ opened: boolean,
61
+ // Whether the slide panel for mobile is currently covered, even partially.
62
+ //
63
+ // This helps us decide what z-index to use: when covered, we want a low
64
+ // z-index, to be below the content; when uncovered, we want a high enough
65
+ // z-index to be clickable.
66
+ slidePanelCovered: boolean,
67
+ // Whether the user seems to be managing their keyboard focus. If so, we
68
+ // auto-focus the `tooltipButton` once the tooltip opens.
69
+ usingKeyboardFocus: boolean,
70
+ // Whether `crossedOut` was true when we last opened the menu.
71
+ //
72
+ // This helps us keep the slide-panel button visually constant, while we
73
+ // slide the content back over it.
74
+ //
75
+ // If we just use `this.props.crossedOut` instead, then the "Cross out"
76
+ // button switches to a different "Re-enable" button _right_ after you tap
77
+ // it, and then immediately starts to disappear. This is visually jarring,
78
+ // and liable to produce anxiety ("wait what was that, what did I miss?").
79
+ // It also gives the false impression that perhaps the user could tap very
80
+ // quickly to toggle it again, which isn't true, and it's not actually an
81
+ // interaction that we want to encourage: interrupting the slide animation
82
+ // would be visually messy, and would be liable to produce a mis-click on
83
+ // the answer choice itself instead of its menu.
84
+ wasCrossedOutOnLastOpen: boolean,
85
+ ...
86
+ };
87
+
88
+ export default class CrossOutMenuWrapper extends React.PureComponent<
89
+ CrossOutMenuWrapperProps,
90
+ CrossOutMenuWrapperState,
91
+ > {
92
+ // A reference to our menu element.
93
+ _menu: ?HTMLDivElement;
94
+
95
+ // A reference to our menu button element.
96
+ _menuButton: ?HTMLButtonElement;
97
+
98
+ // Whether keyboard focus is currently inside a CrossOutButton.
99
+ _crossOutButtonIsFocused: boolean;
100
+
101
+ state: CrossOutMenuWrapperState = {
102
+ hovered: false,
103
+ opened: false,
104
+ slidePanelCovered: true,
105
+ usingKeyboardFocus: false,
106
+ wasCrossedOutOnLastOpen: false,
107
+ };
108
+
109
+ UNSAFE_componentWillReceiveProps(nextProps: CrossOutMenuWrapperProps) {
110
+ // If the menu becomes disabled, it should also become closed. (This
111
+ // helps us avoid permanently staying open when switching from
112
+ // non-review mode to review mode!)
113
+ if (!nextProps.enabled) {
114
+ this._closeMenu();
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Open the menu, and shift focus to the tooltip cross-out button, if
120
+ * necessary.
121
+ *
122
+ * NOTE(mdr): We only do focus-shifting for the tooltip case, because it's
123
+ * especially hard to tab-navigate to. But the slide panel button is
124
+ * easy to tab to, so we don't do any special focus behavior.
125
+ */
126
+ _openMenu: () => void = () => {
127
+ // If keyboard focus is inside this component (e.g., the user
128
+ // tabbed to it and pressed Enter), set state that will cause us to
129
+ // auto-focus the tooltip content once it mounts. This is helpful
130
+ // if you care about element focus (e.g., primary keyboard user),
131
+ // but distracting if you don't (e.g., primary mouse user).
132
+ const thisComponentHasFocus =
133
+ this._menu != null && this._menu.contains(document.activeElement);
134
+
135
+ this.setState({
136
+ opened: true,
137
+ usingKeyboardFocus: thisComponentHasFocus,
138
+ wasCrossedOutOnLastOpen: this.props.crossedOut,
139
+ });
140
+ };
141
+
142
+ /**
143
+ * Close the menu, and shift focus back out of the button, if necessary.
144
+ */
145
+ _closeMenu: () => void = () => {
146
+ // If we're closing the menu, but keyboard focus is still inside the
147
+ // menu tooltip, move focus back to the menu button.
148
+ if (this._crossOutButtonIsFocused && this._menuButton) {
149
+ this._menuButton.focus();
150
+ }
151
+
152
+ this.setState({opened: false, slidePanelCovered: true});
153
+ };
154
+
155
+ /**
156
+ * When the menu is clicked, we call this handler to toggle the menu
157
+ * open/closed.
158
+ */
159
+ _handleMenuClick: (
160
+ e: SyntheticMouseEvent<HTMLElement>,
161
+ willBeOpened: boolean,
162
+ ) => void = (
163
+ e: SyntheticMouseEvent<HTMLElement>,
164
+ willBeOpened: boolean,
165
+ ) => {
166
+ // Toggle the `opened` state. (Instead of reading `!this.state.opened`
167
+ // here, we use the value at the time the element was rendered. This
168
+ // helps us avoid double-toggling if we receive events from both the
169
+ // `menuButton` and `menuWrapper`.)
170
+ if (willBeOpened) {
171
+ this._openMenu();
172
+ } else {
173
+ this._closeMenu();
174
+ }
175
+
176
+ // This click shouldn't count as a click on the parent <label> element.
177
+ // TODO(mdr): Should we move the <label> to not include this menu?
178
+ e.preventDefault();
179
+ };
180
+
181
+ /**
182
+ * When the tooltip is clicked, we call this handler to toggle the
183
+ * cross-out state and close the tooltip.
184
+ */
185
+ _handleCrossedOutButtonClick: (willBeCrossedOut: boolean) => void = (
186
+ willBeCrossedOut: boolean,
187
+ ) => {
188
+ // Toggle the `crossedOut` state. (Instead of reading
189
+ // `!this.props.crossedOut` here, we use the value at the time the
190
+ // element was rendered. This helps us avoid double-toggling if we
191
+ // receive events from both the `menuButton` and `menuWrapper`.)
192
+ //
193
+ // NOTE(mdr): This *shouldn't* be an issue, because the current React
194
+ // lifecycle seems to not give us new props during a click
195
+ // handler's bubbling... but it seems good to be defensive!
196
+ this.props.onCrossedOutChange(willBeCrossedOut);
197
+ this._closeMenu();
198
+ };
199
+
200
+ /**
201
+ * When we finish the slide-open animation, record that the slide panel is
202
+ * now uncovered.
203
+ */
204
+ _handleSlideTransitionEnd: () => void = () => {
205
+ if (this.state.opened) {
206
+ this.setState({slidePanelCovered: false});
207
+ }
208
+ };
209
+
210
+ /**
211
+ * Build props for the `CrossOutButton`, which we'll use in both the
212
+ * tooltip's button (desktop) and the slide panel's button (mobile).
213
+ */
214
+ _getButtonProps(theme: "day" | "night"): {|
215
+ crossedOut: boolean,
216
+ disabled?: boolean,
217
+ onBlur: () => void,
218
+ onClick: () => void,
219
+ onFocus: () => void,
220
+ pos: number,
221
+ primaryProductColor?: string,
222
+ tabFocusable?: boolean,
223
+ theme: "day" | "night",
224
+ usingKeyboardFocus: boolean,
225
+ |} {
226
+ const wasCrossedOut = this.state.wasCrossedOutOnLastOpen;
227
+
228
+ return {
229
+ onClick: () => {
230
+ this._handleCrossedOutButtonClick(!wasCrossedOut);
231
+ },
232
+ onFocus: () => {
233
+ this._crossOutButtonIsFocused = true;
234
+ },
235
+ onBlur: () => {
236
+ this._crossOutButtonIsFocused = false;
237
+ },
238
+ crossedOut: wasCrossedOut,
239
+ pos: this.props.pos,
240
+ primaryProductColor: this.props.primaryProductColor,
241
+ usingKeyboardFocus: this.state.usingKeyboardFocus,
242
+ theme,
243
+ };
244
+ }
245
+
246
+ _getMenuIconColor(): void | string {
247
+ if (this.state.opened) {
248
+ return this.props.primaryProductColor;
249
+ }
250
+ if (this.state.hovered) {
251
+ return colors.offBlack;
252
+ }
253
+ return colors.offBlack50;
254
+ }
255
+
256
+ _renderMenu(): React.Element<"div"> {
257
+ const {pos, isSatProduct} = this.props;
258
+ const {opened} = this.state;
259
+ const menuStyles = css(styles.menu, isSatProduct && styles.spacing);
260
+
261
+ return (
262
+ // This whole wrapper is a valid click target for opening the menu,
263
+ // and it's where we track hover state. But this isn't the main
264
+ // _semantic_ click target, and it's not focusable; see the
265
+ // <button>.
266
+ <div
267
+ ref={(el) => (this._menu = el)}
268
+ className={menuStyles}
269
+ onClick={(e) => this._handleMenuClick(e, !opened)}
270
+ onMouseEnter={() => this.setState({hovered: true})}
271
+ onMouseLeave={() => this.setState({hovered: false})}
272
+ >
273
+ <CrossOutTooltip
274
+ // This tooltip is "dismissed" unless the menu is open.
275
+ dismissed={!this.state.opened}
276
+ onDismiss={this._closeMenu}
277
+ buttonProps={this._getButtonProps("day")}
278
+ >
279
+ {/* This button is the main semantic click target. It
280
+ * receives focus when you tab to it, and has a nice small
281
+ * square focus outline. But the entire wrapper element is
282
+ * a valid click target, too! */}
283
+ <button
284
+ type="button"
285
+ aria-label={i18n._("Open menu for Choice %(letter)s", {
286
+ letter: getChoiceLetter(pos),
287
+ })}
288
+ className={css([styles.menuButton])}
289
+ onClick={(e) => this._handleMenuClick(e, !opened)}
290
+ // Don't focus this element on mousedown. It's visually
291
+ // distracting, and confuses our "should we shift focus"
292
+ // logic for keyboard users.
293
+ onMouseDown={(e) => e.preventDefault()}
294
+ ref={(el) => (this._menuButton = el)}
295
+ >
296
+ <Icon
297
+ className={css(styles.menuIcon)}
298
+ icon={ellipsisHorizontalIcon}
299
+ size={3}
300
+ color={this._getMenuIconColor()}
301
+ />
302
+ </button>
303
+ </CrossOutTooltip>
304
+ </div>
305
+ );
306
+ }
307
+
308
+ _renderSlidePanel(): React.Element<"div"> {
309
+ const buttonProps = this._getButtonProps("night");
310
+ const {slidePanelCovered} = this.state;
311
+
312
+ return (
313
+ <div
314
+ // The whole slide panel acts as an extra click target, but the
315
+ // semantic click target (and focusable element) is the button
316
+ // itself.
317
+ className={css(
318
+ styles.slidePanel,
319
+ slidePanelCovered && styles.slidePanelCovered,
320
+ )}
321
+ onClick={buttonProps.onClick}
322
+ aria-hidden={slidePanelCovered}
323
+ >
324
+ <CrossOutButton
325
+ {...buttonProps}
326
+ disabled={slidePanelCovered}
327
+ tabFocusable={!slidePanelCovered}
328
+ />
329
+ </div>
330
+ );
331
+ }
332
+
333
+ render(): React.Element<"div"> {
334
+ const {enabled} = this.props;
335
+ const {opened} = this.state;
336
+
337
+ // There are three main visual elements being composed here:
338
+ //
339
+ // 1. The choice content, provided as `this.props.children`.
340
+ // 2. The menu element (ellipsis), which toggles the menu open/closed.
341
+ // (The tooltip on desktop is included as part of the menu element.)
342
+ // 3. The slide panel, which sits beneath the other elements unless the
343
+ // menu is open and we're on mobile.
344
+ //
345
+ // On mobile, in order to show the slide panel, we need to slide
346
+ // elements #1 and #2 to the left. So, we wrap them together in a
347
+ // `slidingContent` element, which controls the actual slide animation.
348
+ //
349
+ // The slide panel itself is a sibling of `slideContent`, and it's
350
+ // absolute-positioned relative to the wrapper. It has a negative
351
+ // z-index when covered.
352
+ //
353
+ // Incidentally, we keep using all these same wrapper elements, even
354
+ // when `this.props.enabled` is false. This helps performance when
355
+ // switching from enabled (non-review mode) to disabled (review mode),
356
+ // because we can reuse the same DOM tree, and just remove the menu and
357
+ // slide panel elements.
358
+ return (
359
+ <div className={css(styles.wrapper)}>
360
+ <div
361
+ className={css(
362
+ styles.slidingContent,
363
+ enabled && styles.slidingContentEnabled,
364
+ opened && styles.slidingContentOpen,
365
+ )}
366
+ onTransitionEnd={this._handleSlideTransitionEnd}
367
+ >
368
+ <div className={css(styles.content)}>
369
+ {this.props.children}
370
+ </div>
371
+ {enabled && this._renderMenu()}
372
+ </div>
373
+ {enabled && this._renderSlidePanel()}
374
+ </div>
375
+ );
376
+ }
377
+ }
378
+
379
+ const SLIDE_PANEL_WIDTH = 96;
380
+
381
+ const styles = StyleSheet.create({
382
+ wrapper: {
383
+ display: "block", // HACK(mdr): Override `display: inline` in LESS :/
384
+ position: "relative",
385
+ },
386
+
387
+ slidingContent: {
388
+ display: "flex",
389
+ flexDirection: "row",
390
+ alignItems: "stretch",
391
+ },
392
+
393
+ slidingContentEnabled: {
394
+ // Only apply this transition when the menu is enabled. That way, when
395
+ // we switch from enabled to disabled with the menu open, we snap right
396
+ // to being closed, rather than awkwardly _sliding_ closed.
397
+ [mediaQueries.smOrSmaller]: {
398
+ transition: "transform 0.2s ease-out",
399
+ backgroundColor: colors.white,
400
+ },
401
+ },
402
+
403
+ slidingContentOpen: {
404
+ [mediaQueries.smOrSmaller]: {
405
+ transform: `translateX(-${SLIDE_PANEL_WIDTH}px)`,
406
+ },
407
+ },
408
+
409
+ content: {
410
+ display: "block", // HACK(mdr): Override `display: inline` in LESS :/
411
+ flex: "1 1 0",
412
+ },
413
+
414
+ menu: {
415
+ cursor: "pointer",
416
+ boxSizing: "border-box",
417
+ width: 32,
418
+ display: "flex",
419
+ flexDirection: "column",
420
+ alignItems: "flex-end",
421
+ justifyContent: "center",
422
+
423
+ // On phone screens, the multiple-choice answer is flush with the
424
+ // viewport, which is confusing and makes for a bad click target. So,
425
+ // we want to give some extra space between the dots and the edge of
426
+ // the screen. But, on desktop, it's nicer to align the dots to the
427
+ // right edge of the answer.
428
+ //
429
+ // HACK(mdr): The point at which answers _actually_ end up flush with
430
+ // the viewport depends on the embedding application. This media
431
+ // query is just a heuristic. But the in-between cases look fine
432
+ // with the extra space anyway, so, well, thems the hacks!
433
+ [mediaQueries.smOrSmaller]: {
434
+ paddingRight: 8,
435
+ },
436
+ },
437
+
438
+ menuButton: {
439
+ // Reset <button> styles to be more like <div>.
440
+ background: "none",
441
+ border: "none",
442
+ font: "inherit",
443
+ padding: 0,
444
+ textAlign: "inherit",
445
+
446
+ cursor: "pointer",
447
+ display: "flex",
448
+ flexDirection: "column",
449
+ justifyContent: "center",
450
+ height: 16,
451
+ },
452
+
453
+ menuIcon: {
454
+ // The target of a NewTooltip must be a block-like element.
455
+ display: "block",
456
+ },
457
+
458
+ slidePanel: {
459
+ position: "absolute",
460
+ top: 0,
461
+ right: 0,
462
+
463
+ boxSizing: "border-box",
464
+ width: SLIDE_PANEL_WIDTH,
465
+ height: "100%",
466
+ paddingLeft: 8,
467
+ paddingRight: 8,
468
+
469
+ display: "flex",
470
+ flexDirection: "row",
471
+ alignItems: "center",
472
+ justifyContent: "center",
473
+
474
+ background: colors.offBlack50,
475
+ cursor: "pointer",
476
+
477
+ [mediaQueries.mdOrLarger]: {
478
+ display: "none",
479
+ },
480
+ },
481
+
482
+ spacing: {
483
+ paddingRight: 12,
484
+ },
485
+
486
+ slidePanelCovered: {
487
+ // When covered, use a z-index that puts us below the block-layout
488
+ // content. This also makes the slide panel unclickable, so we _only_
489
+ // apply it when the panel should be covered.
490
+ zIndex: -1,
491
+ },
492
+ });