@khanacademy/perseus 0.0.1 → 0.2.1
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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +29 -0
- package/dist/es/index.css +4361 -0
- package/dist/es/index.js +7 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +4361 -0
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +148 -46
- package/package.json +50 -4
- package/src/README.md +35 -0
- package/src/__stories__/item-renderer.stories.jsx +27 -0
- package/src/__stories__/server-item-renderer.stories.jsx +27 -0
- package/src/__testdata__/item-renderer_testdata.js +161 -0
- package/src/__testdata__/renderer_testdata.js +123 -0
- package/src/__tests__/__snapshots__/item-renderer_test.jsx.snap +233 -0
- package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +516 -0
- package/src/__tests__/__snapshots__/pure-markdown_test.js.snap +113 -0
- package/src/__tests__/__snapshots__/renderer_test.jsx.snap +576 -0
- package/src/__tests__/__snapshots__/server-item-renderer_test.jsx.snap +306 -0
- package/src/__tests__/a11y_test.jsx +304 -0
- package/src/__tests__/item-renderer_test.jsx +892 -0
- package/src/__tests__/mock-asset-loading-widget.jsx +57 -0
- package/src/__tests__/mock-widget.jsx +35 -0
- package/src/__tests__/perseus-markdown_test.jsx +747 -0
- package/src/__tests__/pure-markdown_test.js +552 -0
- package/src/__tests__/renderability_test.jsx +481 -0
- package/src/__tests__/renderer-api_test.jsx +333 -0
- package/src/__tests__/renderer_test.jsx +1792 -0
- package/src/__tests__/server-item-renderer_test.jsx +516 -0
- package/src/__tests__/test-items/image-item.js +25 -0
- package/src/__tests__/test-items/input-number-1-item.js +27 -0
- package/src/__tests__/test-items/input-number-2-item.js +39 -0
- package/src/__tests__/test-items/table-item.js +30 -0
- package/src/__tests__/tex-wrangler_test.js +83 -0
- package/src/__tests__/util_test.js +41 -0
- package/src/a11y.js +46 -0
- package/src/all-widgets.js +9 -0
- package/src/article-renderer.jsx +215 -0
- package/src/asset-context.js +30 -0
- package/src/basic-widgets.js +13 -0
- package/src/components/__stories__/button-group.stories.jsx +61 -0
- package/src/components/__stories__/fixed-to-responsive.stories.jsx +132 -0
- package/src/components/__stories__/graph.stories.jsx +27 -0
- package/src/components/__stories__/graphie.stories.jsx +28 -0
- package/src/components/__stories__/hud.stories.jsx +36 -0
- package/src/components/__stories__/icon.stories.jsx +56 -0
- package/src/components/__stories__/image-loader.stories.jsx +60 -0
- package/src/components/__stories__/info-tip.stories.jsx +33 -0
- package/src/components/__stories__/inline-icon.stories.jsx +39 -0
- package/src/components/__stories__/input-with-examples.stories.jsx +51 -0
- package/src/components/__stories__/lint.stories.jsx +40 -0
- package/src/components/__stories__/math-input.stories.jsx +29 -0
- package/src/components/__stories__/math-output.stories.jsx +24 -0
- package/src/components/__stories__/multi-button-group.stories.jsx +69 -0
- package/src/components/__stories__/number-input.stories.jsx +42 -0
- package/src/components/__stories__/possible-answers.stories.jsx +22 -0
- package/src/components/__stories__/prop-check-box.stories.jsx +62 -0
- package/src/components/__stories__/range-input.stories.jsx +28 -0
- package/src/components/__stories__/simple-keypad-input.stories.jsx +28 -0
- package/src/components/__stories__/sortable.stories.jsx +73 -0
- package/src/components/__stories__/stub-tag-editor.stories.jsx +44 -0
- package/src/components/__stories__/svg-image.stories.jsx +60 -0
- package/src/components/__stories__/tex.stories.jsx +18 -0
- package/src/components/__stories__/text-buttons.stories.jsx +51 -0
- package/src/components/__stories__/text-input.stories.jsx +34 -0
- package/src/components/__stories__/text-list-editor.stories.jsx +33 -0
- package/src/components/__stories__/zoomable-tex.stories.jsx +39 -0
- package/src/components/__stories__/zoomable.stories.jsx +40 -0
- package/src/components/__testdata__/sorter_testdata.js +20 -0
- package/src/components/__tests__/__snapshots__/sortable_test.jsx.snap +79 -0
- package/src/components/__tests__/__snapshots__/sorter_test.jsx.snap +297 -0
- package/src/components/__tests__/number-input_test.jsx +112 -0
- package/src/components/__tests__/sortable_test.jsx +234 -0
- package/src/components/__tests__/sorter_test.jsx +111 -0
- package/src/components/__tests__/zoomable_test.jsx +388 -0
- package/src/components/button-group.jsx +131 -0
- package/src/components/constants.js +15 -0
- package/src/components/fixed-to-responsive.jsx +164 -0
- package/src/components/graph.jsx +415 -0
- package/src/components/graphie-classes.jsx +148 -0
- package/src/components/graphie-movables.jsx +342 -0
- package/src/components/graphie.jsx +353 -0
- package/src/components/highlighting/highlightable-content.jsx +245 -0
- package/src/components/highlighting/highlights.js +208 -0
- package/src/components/highlighting/ranges.js +299 -0
- package/src/components/highlighting/types.js +73 -0
- package/src/components/highlighting/ui/highlight-renderer.jsx +179 -0
- package/src/components/highlighting/ui/highlight-set-renderer.jsx +190 -0
- package/src/components/highlighting/ui/highlight-tooltip.jsx +153 -0
- package/src/components/highlighting/ui/highlighting-ui.jsx +163 -0
- package/src/components/highlighting/ui/selection-tracker.jsx +195 -0
- package/src/components/highlighting/ui/types.js +21 -0
- package/src/components/highlighting/ui/util.js +166 -0
- package/src/components/highlighting/word-indexer.jsx +155 -0
- package/src/components/hud.jsx +179 -0
- package/src/components/icon.jsx +149 -0
- package/src/components/image-loader.jsx +175 -0
- package/src/components/info-tip/info-tip.jsx +146 -0
- package/src/components/info-tip.jsx +38 -0
- package/src/components/inline-icon.jsx +81 -0
- package/src/components/input-with-examples.jsx +231 -0
- package/src/components/lint.jsx +454 -0
- package/src/components/math-input.jsx +282 -0
- package/src/components/math-output.jsx +128 -0
- package/src/components/multi-button-group.jsx +137 -0
- package/src/components/new-tooltip/README.md +110 -0
- package/src/components/new-tooltip/__stories__/flexible-position.stories.jsx +437 -0
- package/src/components/new-tooltip/__stories__/new-tooltip.stories.jsx +665 -0
- package/src/components/new-tooltip/__stories__/tooltip-arrow.stories.jsx +70 -0
- package/src/components/new-tooltip/__stories__/tooltip-bubble.stories.jsx +98 -0
- package/src/components/new-tooltip/__tests__/style-constants_test.js +8 -0
- package/src/components/new-tooltip/__tests__/util_test.js +47 -0
- package/src/components/new-tooltip/flexible-position.jsx +182 -0
- package/src/components/new-tooltip/new-tooltip.jsx +399 -0
- package/src/components/new-tooltip/style-constants.js +13 -0
- package/src/components/new-tooltip/tooltip-arrow.jsx +184 -0
- package/src/components/new-tooltip/tooltip-bubble.jsx +223 -0
- package/src/components/new-tooltip/tooltip-portal.jsx +101 -0
- package/src/components/new-tooltip/tooltip-positioner.jsx +529 -0
- package/src/components/new-tooltip/types.js +183 -0
- package/src/components/new-tooltip/util.js +27 -0
- package/src/components/number-input.jsx +253 -0
- package/src/components/possible-answers.jsx +36 -0
- package/src/components/prop-check-box.jsx +80 -0
- package/src/components/range-input.jsx +61 -0
- package/src/components/simple-keypad-input.jsx +91 -0
- package/src/components/sortable.jsx +873 -0
- package/src/components/stub-tag-editor.jsx +54 -0
- package/src/components/svg-image.jsx +762 -0
- package/src/components/tex-buttons.jsx +322 -0
- package/src/components/tex.jsx +70 -0
- package/src/components/text-input.jsx +82 -0
- package/src/components/text-list-editor.jsx +157 -0
- package/src/components/tooltip.jsx +395 -0
- package/src/components/visibility-observer/__stories__/visibility-observer.stories.jsx +140 -0
- package/src/components/visibility-observer/get-scroll-parent.js +45 -0
- package/src/components/visibility-observer/visibility-observer.js +109 -0
- package/src/components/visibility-observer/visibility-observer.jsx +123 -0
- package/src/components/zoomable-tex.jsx +70 -0
- package/src/components/zoomable.jsx +304 -0
- package/src/components.js +17 -0
- package/src/dependencies.js +22 -0
- package/src/error-boundary.jsx +48 -0
- package/src/extra-widgets.js +75 -0
- package/src/gorgon/README +40 -0
- package/src/gorgon/__tests__/matcher_test.js +497 -0
- package/src/gorgon/__tests__/rule_test.js +101 -0
- package/src/gorgon/__tests__/rules_test.js +487 -0
- package/src/gorgon/__tests__/selector-parser_test.js +52 -0
- package/src/gorgon/__tests__/tree-transformer_test.js +446 -0
- package/src/gorgon/gorgon.js +290 -0
- package/src/gorgon/proptypes.js +29 -0
- package/src/gorgon/rule.js +413 -0
- package/src/gorgon/rules/absolute-url.js +24 -0
- package/src/gorgon/rules/all-rules.js +72 -0
- package/src/gorgon/rules/blockquoted-math.js +10 -0
- package/src/gorgon/rules/blockquoted-widget.js +10 -0
- package/src/gorgon/rules/double-spacing-after-terminal.js +12 -0
- package/src/gorgon/rules/extra-content-spacing.js +12 -0
- package/src/gorgon/rules/heading-level-1.js +14 -0
- package/src/gorgon/rules/heading-level-skip.js +20 -0
- package/src/gorgon/rules/heading-sentence-case.js +11 -0
- package/src/gorgon/rules/heading-title-case.js +63 -0
- package/src/gorgon/rules/image-alt-text.js +21 -0
- package/src/gorgon/rules/image-in-table.js +10 -0
- package/src/gorgon/rules/image-spaces-around-urls.js +35 -0
- package/src/gorgon/rules/image-widget.js +50 -0
- package/src/gorgon/rules/link-click-here.js +11 -0
- package/src/gorgon/rules/lint-utils.js +48 -0
- package/src/gorgon/rules/long-paragraph.js +14 -0
- package/src/gorgon/rules/math-adjacent.js +10 -0
- package/src/gorgon/rules/math-align-extra-break.js +11 -0
- package/src/gorgon/rules/math-align-linebreaks.js +43 -0
- package/src/gorgon/rules/math-empty.js +10 -0
- package/src/gorgon/rules/math-font-size.js +12 -0
- package/src/gorgon/rules/math-frac.js +10 -0
- package/src/gorgon/rules/math-nested.js +11 -0
- package/src/gorgon/rules/math-starts-with-space.js +12 -0
- package/src/gorgon/rules/math-text-empty.js +10 -0
- package/src/gorgon/rules/math-without-dollars.js +14 -0
- package/src/gorgon/rules/nested-lists.js +11 -0
- package/src/gorgon/rules/profanity.js +10 -0
- package/src/gorgon/rules/table-missing-cells.js +20 -0
- package/src/gorgon/rules/unbalanced-code-delimiters.js +14 -0
- package/src/gorgon/rules/unescaped-dollar.js +10 -0
- package/src/gorgon/rules/widget-in-table.js +10 -0
- package/src/gorgon/selector.js +506 -0
- package/src/gorgon/tree-transformer.js +588 -0
- package/src/hint-renderer.jsx +159 -0
- package/src/hints-renderer.jsx +359 -0
- package/src/icon-paths.js +120 -0
- package/src/index.js +149 -2
- package/src/init.js +49 -0
- package/src/interaction-tracker.js +60 -0
- package/src/interactive2/__tests__/movable-mock.js +48 -0
- package/src/interactive2/__tests__/movable-point_test.js +146 -0
- package/src/interactive2/interactive-util.js +152 -0
- package/src/interactive2/movable-helper-methods.js +160 -0
- package/src/interactive2/movable-line-options.js +392 -0
- package/src/interactive2/movable-line.js +322 -0
- package/src/interactive2/movable-point-options.js +190 -0
- package/src/interactive2/movable-point.jsx +603 -0
- package/src/interactive2/movable-polygon-options.js +351 -0
- package/src/interactive2/movable-polygon.js +368 -0
- package/src/interactive2/movable.js +273 -0
- package/src/interactive2/objective_.js +79 -0
- package/src/interactive2/types.js +3 -0
- package/src/interactive2/wrapped-defaults.js +86 -0
- package/src/interactive2/wrapped-ellipse.js +87 -0
- package/src/interactive2/wrapped-line.js +109 -0
- package/src/interactive2/wrapped-path.js +47 -0
- package/src/interactive2.js +91 -0
- package/src/item-renderer.jsx +518 -0
- package/src/item-version.js +12 -0
- package/src/jipt-paragraphs.js +44 -0
- package/src/jquery.mobile.vmouse.js +545 -0
- package/src/keypad-context.js +33 -0
- package/src/loading-context.js +20 -0
- package/src/logging/log.js +107 -0
- package/src/logging/log_test.js +41 -0
- package/src/mixins/changeable.jsx +115 -0
- package/src/mixins/editor-jsonify.jsx +13 -0
- package/src/mixins/provide-keypad.jsx +129 -0
- package/src/mixins/widget-jsonify-deprecated.jsx +23 -0
- package/src/mixins/widget-prop-blacklist.js +36 -0
- package/src/multi-items/__stories__/multi-renderer.stories.js +88 -0
- package/src/multi-items/__testdata__/multi-renderer_testdata.js +174 -0
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +994 -0
- package/src/multi-items/__tests__/items_test.js +245 -0
- package/src/multi-items/__tests__/multi-renderer_test.js +903 -0
- package/src/multi-items/__tests__/prop-type-builders_test.js +134 -0
- package/src/multi-items/__tests__/shapes_test.js +44 -0
- package/src/multi-items/__tests__/trees_test.js +316 -0
- package/src/multi-items/item-types.js +55 -0
- package/src/multi-items/items.js +176 -0
- package/src/multi-items/multi-renderer.jsx +543 -0
- package/src/multi-items/prop-type-builders.js +105 -0
- package/src/multi-items/shape-types.js +47 -0
- package/src/multi-items/shapes.js +53 -0
- package/src/multi-items/tree-types.js +42 -0
- package/src/multi-items/trees.js +345 -0
- package/src/multi-items.js +76 -0
- package/src/not-gorgon.js +129 -0
- package/src/perseus-api.jsx +270 -0
- package/src/perseus-env.js +26 -0
- package/src/perseus-error.js +19 -0
- package/src/perseus-markdown.jsx +433 -0
- package/src/perseus-types.js +1597 -0
- package/src/pure-markdown.js +347 -0
- package/src/question-paragraph.jsx +30 -0
- package/src/renderability.jsx +108 -0
- package/src/renderer.jsx +1932 -0
- package/src/server-item-renderer.jsx +467 -0
- package/src/sigfigs.jsx +492 -0
- package/src/styles/articles.less +748 -0
- package/src/styles/constants.js +119 -0
- package/src/styles/global-constants.js +350 -0
- package/src/styles/global-styles.js +307 -0
- package/src/styles/khan-exercise.css +1304 -0
- package/src/styles/media-queries.js +48 -0
- package/src/styles/perseus-renderer.less +1007 -0
- package/src/styles/shared.js +132 -0
- package/src/styles/util.less +47 -0
- package/src/styles/variables.boostrap.less +301 -0
- package/src/styles/variables.less +68 -0
- package/src/styles/variables.shared.less +356 -0
- package/src/styles/widgets/categorizer.less +39 -0
- package/src/styles/widgets/definition.less +86 -0
- package/src/styles/widgets/dropdown.less +3 -0
- package/src/styles/widgets/expression.less +147 -0
- package/src/styles/widgets/graded-group.less +36 -0
- package/src/styles/widgets/grapher.less +11 -0
- package/src/styles/widgets/group.less +19 -0
- package/src/styles/widgets/image.less +29 -0
- package/src/styles/widgets/interactive-graph.less +61 -0
- package/src/styles/widgets/label-image.less +41 -0
- package/src/styles/widgets/matcher.less +8 -0
- package/src/styles/widgets/matrix.less +120 -0
- package/src/styles/widgets/measurer.less +20 -0
- package/src/styles/widgets/orderer.less +97 -0
- package/src/styles/widgets/passage.less +151 -0
- package/src/styles/widgets/plotter.less +33 -0
- package/src/styles/widgets/radio.less +52 -0
- package/src/styles/widgets/reaction-diagram.less +35 -0
- package/src/styles/widgets/sequence.less +5 -0
- package/src/styles/widgets/sortable.less +127 -0
- package/src/styles/widgets/table.less +56 -0
- package/src/styles/widgets/transformer.less +28 -0
- package/src/styles/widgets/unit.less +14 -0
- package/src/tex-wrangler.js +176 -0
- package/src/traversal.jsx +141 -0
- package/src/types.js +538 -0
- package/src/util/a11y.js +24 -0
- package/src/util/answer-types.js +862 -0
- package/src/util/answer-types_test.js +115 -0
- package/src/util/colors.js +94 -0
- package/src/util/fix-passage-refs.jsx +122 -0
- package/src/util/graph-utils.js +16 -0
- package/src/util/graphie.js +1586 -0
- package/src/util/interactive.js +4078 -0
- package/src/util/katex-preprocess.js +16 -0
- package/src/util/math.js +296 -0
- package/src/util/mobile-native-utils.js +14 -0
- package/src/util/mobile-native-utils_test.js +24 -0
- package/src/util/register-all-widgets-for-testing.js +12 -0
- package/src/util/scroll-utils.js +162 -0
- package/src/util/sizing-utils.js +46 -0
- package/src/util/tex.js +203 -0
- package/src/util/url-utils.js +36 -0
- package/src/util.js +990 -0
- package/src/version.js +11 -0
- package/src/widget-container.jsx +174 -0
- package/src/widgets/__stories__/categorizer.stories.jsx +15 -0
- package/src/widgets/__stories__/cs-program.stories.jsx +15 -0
- package/src/widgets/__stories__/definition.stories.jsx +38 -0
- package/src/widgets/__stories__/dropdown.stories.jsx +15 -0
- package/src/widgets/__stories__/explanation.stories.jsx +19 -0
- package/src/widgets/__stories__/expression.stories.jsx +26 -0
- package/src/widgets/__stories__/graded-group-set.stories.jsx +32 -0
- package/src/widgets/__stories__/graded-group.stories.jsx +32 -0
- package/src/widgets/__stories__/grapher.stories.jsx +47 -0
- package/src/widgets/__stories__/group.stories.jsx +15 -0
- package/src/widgets/__stories__/iframe.stories.jsx +15 -0
- package/src/widgets/__stories__/image.stories.jsx +62 -0
- package/src/widgets/__stories__/input-number.stories.jsx +90 -0
- package/src/widgets/__stories__/interaction.stories.jsx +15 -0
- package/src/widgets/__stories__/interactive-graph.stories.jsx +61 -0
- package/src/widgets/__stories__/matcher.stories.jsx +15 -0
- package/src/widgets/__stories__/matrix.stories.jsx +15 -0
- package/src/widgets/__stories__/number-line.stories.jsx +15 -0
- package/src/widgets/__stories__/numeric-input.stories.jsx +19 -0
- package/src/widgets/__stories__/orderer.stories.jsx +15 -0
- package/src/widgets/__stories__/passage-ref.stories.jsx +19 -0
- package/src/widgets/__stories__/passage.stories.jsx +19 -0
- package/src/widgets/__stories__/radio.stories.jsx +82 -0
- package/src/widgets/__stories__/sorter.stories.jsx +15 -0
- package/src/widgets/__stories__/transformer.stories.jsx +15 -0
- package/src/widgets/__stories__/unit.stories.jsx +45 -0
- package/src/widgets/__stories__/video-transcript-link.stories.jsx +24 -0
- package/src/widgets/__stories__/video.stories.jsx +15 -0
- package/src/widgets/__testdata__/categorizer_testdata.js +50 -0
- package/src/widgets/__testdata__/cs-program_testdata.js +28 -0
- package/src/widgets/__testdata__/dropdown_testdata.js +34 -0
- package/src/widgets/__testdata__/explanation_testdata.js +56 -0
- package/src/widgets/__testdata__/expression_testdata.js +95 -0
- package/src/widgets/__testdata__/graded-group-set_testdata.js +358 -0
- package/src/widgets/__testdata__/graded-group_testdata.js +79 -0
- package/src/widgets/__testdata__/grapher_testdata.js +376 -0
- package/src/widgets/__testdata__/group_testdata.js +156 -0
- package/src/widgets/__testdata__/iframe_testdata.js +36 -0
- package/src/widgets/__testdata__/image_testdata.js +38 -0
- package/src/widgets/__testdata__/input-number_testdata.js +73 -0
- package/src/widgets/__testdata__/interaction_testdata.js +264 -0
- package/src/widgets/__testdata__/interactive-graph_testdata.js +646 -0
- package/src/widgets/__testdata__/matcher_testdata.js +34 -0
- package/src/widgets/__testdata__/matrix_testdata.js +29 -0
- package/src/widgets/__testdata__/number-line_testdata.js +31 -0
- package/src/widgets/__testdata__/numeric-input_testdata.js +266 -0
- package/src/widgets/__testdata__/orderer_testdata.js +58 -0
- package/src/widgets/__testdata__/passage-ref_testdata.js +41 -0
- package/src/widgets/__testdata__/passage_testdata.js +71 -0
- package/src/widgets/__testdata__/radio_testdata.js +210 -0
- package/src/widgets/__testdata__/sorter_testdata.js +20 -0
- package/src/widgets/__testdata__/transformer_testdata.js +109 -0
- package/src/widgets/__testdata__/video_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/categorizer_test.jsx.snap +928 -0
- package/src/widgets/__tests__/__snapshots__/cs-program_test.jsx.snap +75 -0
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +99 -0
- package/src/widgets/__tests__/__snapshots__/dropdown_test.jsx.snap +143 -0
- package/src/widgets/__tests__/__snapshots__/explanation_test.js.snap +473 -0
- package/src/widgets/__tests__/__snapshots__/expression_test.jsx.snap +38 -0
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +754 -0
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +310 -0
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +863 -0
- package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +2880 -0
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +1154 -0
- package/src/widgets/__tests__/__snapshots__/iframe_test.jsx.snap +69 -0
- package/src/widgets/__tests__/__snapshots__/image_test.jsx.snap +388 -0
- package/src/widgets/__tests__/__snapshots__/input-number_test.jsx.snap +978 -0
- package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +311 -0
- package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2915 -0
- package/src/widgets/__tests__/__snapshots__/matcher_test.jsx.snap +1039 -0
- package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +450 -0
- package/src/widgets/__tests__/__snapshots__/number-line_test.jsx.snap +888 -0
- package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +930 -0
- package/src/widgets/__tests__/__snapshots__/orderer_test.jsx.snap +275 -0
- package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +446 -0
- package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +1988 -0
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +7107 -0
- package/src/widgets/__tests__/__snapshots__/video_test.jsx.snap +163 -0
- package/src/widgets/__tests__/categorizer_test.jsx +205 -0
- package/src/widgets/__tests__/cs-program_test.jsx +45 -0
- package/src/widgets/__tests__/definition_test.js +152 -0
- package/src/widgets/__tests__/dropdown_test.jsx +106 -0
- package/src/widgets/__tests__/explanation_test.js +199 -0
- package/src/widgets/__tests__/expression_test.jsx +439 -0
- package/src/widgets/__tests__/graded-group-set-jipt_test.jsx +33 -0
- package/src/widgets/__tests__/graded-group-set_test.jsx +270 -0
- package/src/widgets/__tests__/graded-group_test.jsx +233 -0
- package/src/widgets/__tests__/grapher.cypress.jsx +486 -0
- package/src/widgets/__tests__/grapher_test.jsx +35 -0
- package/src/widgets/__tests__/group_test.jsx +512 -0
- package/src/widgets/__tests__/iframe_test.jsx +47 -0
- package/src/widgets/__tests__/image_test.jsx +65 -0
- package/src/widgets/__tests__/input-number_test.jsx +360 -0
- package/src/widgets/__tests__/interaction_test.jsx +33 -0
- package/src/widgets/__tests__/interactive-graph.cypress.jsx +697 -0
- package/src/widgets/__tests__/interactive-graph_test.jsx +105 -0
- package/src/widgets/__tests__/matcher_test.jsx +157 -0
- package/src/widgets/__tests__/matrix_test.jsx +84 -0
- package/src/widgets/__tests__/number-line_test.jsx +81 -0
- package/src/widgets/__tests__/numeric-input_test.jsx +574 -0
- package/src/widgets/__tests__/orderer_test.jsx +75 -0
- package/src/widgets/__tests__/passage-ref_test.jsx +166 -0
- package/src/widgets/__tests__/passage_test.jsx +133 -0
- package/src/widgets/__tests__/radio/base-radio_test.jsx +82 -0
- package/src/widgets/__tests__/radio_test.jsx +651 -0
- package/src/widgets/__tests__/renderQuestion.jsx +66 -0
- package/src/widgets/__tests__/unit_test.jsx +144 -0
- package/src/widgets/__tests__/video_test.jsx +55 -0
- package/src/widgets/categorizer.jsx +338 -0
- package/src/widgets/cs-program.jsx +227 -0
- package/src/widgets/definition.jsx +141 -0
- package/src/widgets/dropdown.jsx +143 -0
- package/src/widgets/explanation.jsx +315 -0
- package/src/widgets/expression.jsx +652 -0
- package/src/widgets/graded-group-answer-bar.jsx +152 -0
- package/src/widgets/graded-group-set.jsx +285 -0
- package/src/widgets/graded-group.jsx +448 -0
- package/src/widgets/grapher/util.jsx +806 -0
- package/src/widgets/grapher.jsx +663 -0
- package/src/widgets/group.jsx +213 -0
- package/src/widgets/iframe.jsx +198 -0
- package/src/widgets/image.jsx +310 -0
- package/src/widgets/input-number.jsx +411 -0
- package/src/widgets/interaction/arrow-picker.jsx +39 -0
- package/src/widgets/interaction/color-picker.jsx +69 -0
- package/src/widgets/interaction/dash-picker.jsx +46 -0
- package/src/widgets/interaction/element-container.jsx +97 -0
- package/src/widgets/interaction.jsx +742 -0
- package/src/widgets/interactive-graph.jsx +2820 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.jsx +93 -0
- package/src/widgets/label-image/__stories__/marker.stories.jsx +172 -0
- package/src/widgets/label-image/answer-choices.jsx +311 -0
- package/src/widgets/label-image/marker.jsx +365 -0
- package/src/widgets/label-image/types.js +33 -0
- package/src/widgets/label-image.jsx +841 -0
- package/src/widgets/label-image_test.js +840 -0
- package/src/widgets/lights-puzzle.jsx +356 -0
- package/src/widgets/matcher.jsx +271 -0
- package/src/widgets/matrix.jsx +616 -0
- package/src/widgets/measurer.jsx +215 -0
- package/src/widgets/molecule/__tests__/layout_test.jsx +159 -0
- package/src/widgets/molecule/__tests__/parser_test.jsx +142 -0
- package/src/widgets/molecule/molecule-drawing.jsx +175 -0
- package/src/widgets/molecule/molecule-layout.jsx +367 -0
- package/src/widgets/molecule/smiles-parser.jsx +300 -0
- package/src/widgets/molecule.jsx +172 -0
- package/src/widgets/number-line.jsx +858 -0
- package/src/widgets/numeric-input.jsx +622 -0
- package/src/widgets/orderer.jsx +753 -0
- package/src/widgets/passage/__tests__/passage-markdown_test.jsx +382 -0
- package/src/widgets/passage/get-line-height-for-node.js +10 -0
- package/src/widgets/passage/passage-markdown.jsx +416 -0
- package/src/widgets/passage-ref-target.jsx +70 -0
- package/src/widgets/passage-ref.jsx +198 -0
- package/src/widgets/passage.jsx +601 -0
- package/src/widgets/plotter.jsx +1198 -0
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +40 -0
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +157 -0
- package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +26 -0
- package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +64 -0
- package/src/widgets/radio/__stories__/cross-out-menu.stories.jsx +59 -0
- package/src/widgets/radio/__stories__/cross-out-tooltip.stories.jsx +59 -0
- package/src/widgets/radio/__stories__/focus-ring.stories.jsx +38 -0
- package/src/widgets/radio/__stories__/option-status.stories.jsx +118 -0
- package/src/widgets/radio/__stories__/toggleable-radio-button.stories.jsx +28 -0
- package/src/widgets/radio/base-radio.jsx +642 -0
- package/src/widgets/radio/choice-icon.jsx +468 -0
- package/src/widgets/radio/choice-none-above.jsx +54 -0
- package/src/widgets/radio/choice.jsx +692 -0
- package/src/widgets/radio/choice.jsx.fixture.js +64 -0
- package/src/widgets/radio/cross-out-button.jsx +136 -0
- package/src/widgets/radio/cross-out-menu-wrapper.jsx +492 -0
- package/src/widgets/radio/cross-out-tooltip.jsx +129 -0
- package/src/widgets/radio/focus-ring.jsx +51 -0
- package/src/widgets/radio/option-status.jsx +89 -0
- package/src/widgets/radio/toggleable-radio-button.jsx +133 -0
- package/src/widgets/radio/util.js +32 -0
- package/src/widgets/radio/util_test.js +19 -0
- package/src/widgets/radio/widget.jsx +470 -0
- package/src/widgets/radio.jsx +156 -0
- package/src/widgets/reaction-diagram.jsx +137 -0
- package/src/widgets/sequence.jsx +140 -0
- package/src/widgets/simulator.jsx +808 -0
- package/src/widgets/sorter.jsx +146 -0
- package/src/widgets/table.jsx +394 -0
- package/src/widgets/transformer.jsx +2876 -0
- package/src/widgets/unit.jsx +372 -0
- package/src/widgets/video-transcript-link.jsx +84 -0
- package/src/widgets/video-transcript-link_test.js +211 -0
- package/src/widgets/video.jsx +126 -0
- package/src/widgets.js +431 -0
- package/src/zoom.js +539 -0
- package/README.md +0 -3
- 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;
|