@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.
- package/.babelrc.js +1 -1
- package/CHANGELOG.md +7 -0
- package/dist/es/index.js +15 -1
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +11934 -0
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/strings.js +148 -1
- package/package.json +40 -4
- package/src/README.md +7 -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 +163 -0
- package/src/components/graph.jsx +414 -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 +149 -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 +456 -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 +78 -0
- package/src/components/range-input.jsx +61 -0
- package/src/components/simple-keypad-input.jsx +91 -0
- package/src/components/sortable.jsx +872 -0
- package/src/components/stub-tag-editor.jsx +53 -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 +604 -0
- package/src/interactive2/movable-polygon-options.js +351 -0
- package/src/interactive2/movable-polygon.js +367 -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 +131 -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 +542 -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 +434 -0
- package/src/perseus-types.js +1597 -0
- package/src/pure-markdown.js +346 -0
- package/src/question-paragraph.jsx +30 -0
- package/src/renderability.jsx +108 -0
- package/src/renderer.jsx +1931 -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 +1008 -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 +1585 -0
- package/src/util/interactive.js +4071 -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__/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 +65 -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 +228 -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 +654 -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 +819 -0
- package/src/widgets/grapher.jsx +658 -0
- package/src/widgets/group.jsx +213 -0
- package/src/widgets/iframe.jsx +199 -0
- package/src/widgets/image.jsx +309 -0
- package/src/widgets/input-number.jsx +410 -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 +2821 -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 +615 -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 +136 -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 +395 -0
- package/src/widgets/transformer.jsx +2870 -0
- package/src/widgets/unit.jsx +371 -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 +131 -0
- package/src/widgets.js +431 -0
- package/src/zoom.js +539 -0
- 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
|
+
});
|