@khanacademy/perseus 0.4.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.eslintrc.js +6 -1
  2. package/CHANGELOG.md +45 -0
  3. package/dist/es/index.css +15 -8
  4. package/dist/es/index.js +1214 -1404
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +15 -8
  7. package/dist/index.js +1217 -1411
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +0 -4
  10. package/package.json +9 -8
  11. package/src/components/number-input.jsx +2 -2
  12. package/src/components/simple-keypad-input.jsx +18 -21
  13. package/src/index.js +1 -1
  14. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
  15. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  16. package/src/perseus-api.jsx +10 -36
  17. package/src/perseus-types.js +18 -145
  18. package/src/styles/constants.js +0 -2
  19. package/src/styles/perseus-renderer.less +8 -2
  20. package/src/styles/variables.less +0 -2
  21. package/src/styles/widgets/passage.less +9 -5
  22. package/src/types.js +18 -35
  23. package/src/util/answer-types.js +2 -3
  24. package/src/util/math.js +3 -10
  25. package/src/util.js +0 -6
  26. package/src/version.js +1 -1
  27. package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
  28. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  29. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  30. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
  31. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
  32. package/src/widgets/__tests__/group_test.jsx +4 -4
  33. package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
  34. package/src/widgets/__tests__/passage_test.jsx +81 -0
  35. package/src/widgets/__tests__/radio/base-radio_test.jsx +291 -41
  36. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  37. package/src/widgets/__tests__/radio/choice_test.jsx +302 -0
  38. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  39. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  40. package/src/widgets/__tests__/radio_test.jsx +39 -15
  41. package/src/widgets/definition.jsx +1 -1
  42. package/src/widgets/group.jsx +84 -102
  43. package/src/widgets/input-number.jsx +6 -1
  44. package/src/widgets/numeric-input.jsx +87 -80
  45. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  46. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  47. package/src/widgets/passage/passage-markdown.jsx +176 -120
  48. package/src/widgets/passage.jsx +48 -46
  49. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  50. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  51. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  52. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  53. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  54. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  55. package/src/widgets/radio/base-radio.jsx +485 -556
  56. package/src/widgets/radio/choice-a11y-text.js +62 -0
  57. package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
  58. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  59. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
  60. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -0
  61. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  62. package/src/widgets/radio/choice-none-above.jsx +49 -40
  63. package/src/widgets/radio/choice.jsx +319 -347
  64. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  65. package/src/widgets/radio/focus-ring.jsx +29 -22
  66. package/src/widgets/radio/option-status.jsx +58 -36
  67. package/src/widgets/radio/widget.jsx +181 -136
  68. package/src/widgets/radio.jsx +16 -17
  69. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  70. package/src/widgets/radio/choice-icon.jsx +0 -469
  71. package/src/widgets/radio/cross-out-button.jsx +0 -134
@@ -0,0 +1,62 @@
1
+ // @flow
2
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
3
+
4
+ function getA11yText(
5
+ letter: string,
6
+ checked: boolean,
7
+ correct: boolean,
8
+ crossedOut: boolean,
9
+ showCorrectness: boolean,
10
+ ): string {
11
+ // There are two pieces of metadata we want to add to each a11yText:
12
+ // whether the answer was checked/crossed-out/neither, and whether the
13
+ // answer is correct/incorrect/not-yet-revealed.
14
+ //
15
+ // Translation is tricky for cross-product situations like this, so
16
+ // we've just enumerated all 9 possibilities as separate strings.
17
+ if (showCorrectness && correct) {
18
+ if (checked) {
19
+ return i18n._("(Choice %(letter)s, Checked, Correct)", {
20
+ letter,
21
+ });
22
+ }
23
+ if (crossedOut) {
24
+ return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
25
+ letter,
26
+ });
27
+ }
28
+ return i18n._("(Choice %(letter)s, Correct)", {
29
+ letter,
30
+ });
31
+ }
32
+ if (showCorrectness && !correct) {
33
+ if (checked) {
34
+ return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
35
+ letter,
36
+ });
37
+ }
38
+ if (crossedOut) {
39
+ return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
40
+ letter,
41
+ });
42
+ }
43
+ return i18n._("(Choice %(letter)s, Incorrect)", {
44
+ letter,
45
+ });
46
+ }
47
+ if (checked) {
48
+ return i18n._("(Choice %(letter)s, Checked)", {
49
+ letter,
50
+ });
51
+ }
52
+ if (crossedOut) {
53
+ return i18n._("(Choice %(letter)s, Crossed out)", {
54
+ letter,
55
+ });
56
+ }
57
+ return i18n._("(Choice %(letter)s)", {
58
+ letter,
59
+ });
60
+ }
61
+
62
+ export default getA11yText;
@@ -0,0 +1,87 @@
1
+ // @flow
2
+ /* Component for rendering a letter icon in radio choice */
3
+
4
+ import Color from "@khanacademy/wonder-blocks-color";
5
+ import * as React from "react";
6
+
7
+ import {getChoiceLetter} from "../util.js";
8
+
9
+ import LibraryChoiceIcon from "./library-choice-icon.jsx";
10
+ import SATChoiceIcon from "./sat-choice-icon.jsx";
11
+
12
+ type ChoiceIconProps = {|
13
+ pos: number,
14
+ checked: boolean,
15
+ crossedOut: boolean,
16
+ focused: boolean,
17
+ hovered: boolean,
18
+ pressed: boolean,
19
+ correct: boolean,
20
+ showCorrectness: boolean,
21
+ // TODO(amy): if we go this "product" flag route, define this type
22
+ // somewhere shared
23
+ product: "sat" | "library",
24
+ multipleSelect: boolean,
25
+ reviewMode: boolean,
26
+ previouslyAnswered: boolean,
27
+ // TODO(mdr): The CrossOutButton needs a transparent-background ChoiceIcon,
28
+ // so I've added this prop. I'm not sure why we have backgrounds in the
29
+ // general case, though? When does the choice container have a
30
+ // non-white background, aside from SAT, which uses a different icon?
31
+ transparentBackground?: boolean,
32
+
33
+ primaryProductColor?: string,
34
+ |};
35
+
36
+ function ChoiceIcon(props: ChoiceIconProps): React.Node {
37
+ const {
38
+ pos,
39
+ reviewMode,
40
+ checked,
41
+ crossedOut,
42
+ correct,
43
+ product,
44
+ multipleSelect,
45
+ showCorrectness,
46
+ focused,
47
+ hovered,
48
+ pressed,
49
+ primaryProductColor = Color.blue,
50
+ previouslyAnswered,
51
+ transparentBackground,
52
+ } = props;
53
+
54
+ const letter = getChoiceLetter(pos);
55
+
56
+ if (product === "sat") {
57
+ return (
58
+ <SATChoiceIcon
59
+ letter={letter}
60
+ reviewMode={reviewMode}
61
+ checked={checked}
62
+ correct={correct}
63
+ crossedOut={crossedOut}
64
+ multipleSelect={multipleSelect}
65
+ />
66
+ );
67
+ }
68
+ return (
69
+ <LibraryChoiceIcon
70
+ letter={letter}
71
+ reviewMode={reviewMode}
72
+ checked={checked}
73
+ crossedOut={crossedOut}
74
+ focused={focused}
75
+ hovered={hovered}
76
+ pressed={pressed}
77
+ correct={correct}
78
+ showCorrectness={showCorrectness}
79
+ primaryProductColor={primaryProductColor}
80
+ previouslyAnswered={previouslyAnswered}
81
+ transparentBackground={transparentBackground}
82
+ multipleSelect={multipleSelect}
83
+ />
84
+ );
85
+ }
86
+
87
+ export default ChoiceIcon;
@@ -0,0 +1,55 @@
1
+ // @flow
2
+ /* Component for rendering a letter icon in radio choice */
3
+
4
+ import {StyleSheet, css} from "aphrodite";
5
+ import * as React from "react";
6
+
7
+ import {SAT_ICON_SIZE, LIBRARY_ICON_SIZE} from "./shared-styles.js";
8
+
9
+ // The "cross-out line" SVG is a bit bigger than the library icon, to provide
10
+ // extra space for the cute rounded line cap.
11
+ const CROSS_OUT_LINE_PADDING = 2;
12
+ const CROSS_OUT_LINE_SIZE = LIBRARY_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
13
+ const SAT_CROSS_OUT_LINE_SIZE = SAT_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
14
+
15
+ /**
16
+ * The "cross-out line" that appears over the icon when the choice has been
17
+ * `crossedOut`.
18
+ */
19
+ function CrossOutLine(props: {color: string, sat?: boolean, ...}): React.Node {
20
+ const crossOutLineSize = props.sat
21
+ ? SAT_CROSS_OUT_LINE_SIZE
22
+ : CROSS_OUT_LINE_SIZE;
23
+ return (
24
+ <svg
25
+ width={crossOutLineSize}
26
+ height={crossOutLineSize}
27
+ viewBox={`0 0 ${crossOutLineSize} ${crossOutLineSize}`}
28
+ className={css(styles.crossOutLine)}
29
+ >
30
+ <line
31
+ // The line stretches from the bottom-left to top-right.
32
+ // We don't quite go to the _very_ corner, because the cute
33
+ // rounded line cap needs to bleed into our padding.
34
+ x1={CROSS_OUT_LINE_PADDING}
35
+ x2={crossOutLineSize - CROSS_OUT_LINE_PADDING}
36
+ y1={crossOutLineSize - CROSS_OUT_LINE_PADDING}
37
+ y2={CROSS_OUT_LINE_PADDING}
38
+ stroke={props.color}
39
+ strokeWidth="2"
40
+ strokeLinecap="round"
41
+ />
42
+ </svg>
43
+ );
44
+ }
45
+
46
+ const styles = StyleSheet.create({
47
+ crossOutLine: {
48
+ // Center the icon within the container.
49
+ position: "absolute",
50
+ top: `calc(50% - ${CROSS_OUT_LINE_SIZE / 2}px)`,
51
+ left: `calc(50% - ${CROSS_OUT_LINE_SIZE / 2}px)`,
52
+ },
53
+ });
54
+
55
+ export default CrossOutLine;
@@ -0,0 +1,214 @@
1
+ // @flow
2
+ /* Component for rendering a letter icon in a library radio choice */
3
+
4
+ import Color from "@khanacademy/wonder-blocks-color";
5
+ import {StyleSheet, css} from "aphrodite";
6
+ import * as React from "react";
7
+
8
+ import InlineIcon from "../../../components/inline-icon.jsx";
9
+ import {iconCheck, iconMinus} from "../../../icon-paths.js";
10
+ import * as styleConstants from "../../../styles/constants.js";
11
+ import FocusRing from "../focus-ring.jsx";
12
+
13
+ import CrossOutLine from "./cross-out-line.jsx";
14
+ import sharedStyles, {LIBRARY_ICON_SIZE} from "./shared-styles.js";
15
+
16
+ type LibraryChoiceIconProps = {
17
+ letter: string,
18
+ checked: boolean,
19
+ crossedOut: boolean,
20
+ focused: boolean,
21
+ hovered: boolean,
22
+ pressed: boolean,
23
+ multipleSelect: boolean,
24
+ correct: ?boolean,
25
+ reviewMode: boolean,
26
+ showCorrectness: boolean,
27
+ primaryProductColor: string,
28
+ previouslyAnswered: boolean,
29
+ transparentBackground?: boolean,
30
+ };
31
+
32
+ function ChoiceInner(props: {
33
+ letter: string,
34
+ showCorrectness: boolean,
35
+ correct: ?boolean,
36
+ }) {
37
+ const {letter, showCorrectness, correct} = props;
38
+
39
+ if (!showCorrectness) {
40
+ return letter;
41
+ }
42
+ if (correct) {
43
+ return (
44
+ <InlineIcon
45
+ {...iconCheck}
46
+ style={{
47
+ position: "relative",
48
+ top: -1,
49
+ }}
50
+ />
51
+ );
52
+ }
53
+ return <InlineIcon {...iconMinus} />;
54
+ }
55
+
56
+ // Handle dynamic styling of the multiple choice icon. Most
57
+ // MC icon styles are constant, but we do allow the caller
58
+ // to specify the selected color, and thus must control styles
59
+ // related to the selected state dynamically.
60
+ function getDynamicStyles(
61
+ checked: boolean,
62
+ showCorrectness: boolean,
63
+ pressed: boolean,
64
+ multipleSelect: boolean,
65
+ primaryProductColor: string,
66
+ correct: ?boolean,
67
+ transparentBackground?: boolean,
68
+ ): {
69
+ backgroundColor: ?string,
70
+ borderColor: string,
71
+ color: string,
72
+ borderRadius: number,
73
+ } {
74
+ let backgroundColor;
75
+ let borderColor;
76
+ let color;
77
+ if (!showCorrectness && pressed) {
78
+ borderColor = primaryProductColor;
79
+ color = primaryProductColor;
80
+ backgroundColor = transparentBackground
81
+ ? "transparent"
82
+ : styleConstants.white;
83
+ } else if (checked) {
84
+ // Note: kaGreen is not only the default product color,
85
+ // but also the "correctness" color
86
+ const bg =
87
+ showCorrectness && correct ? Color.green : primaryProductColor;
88
+ color = styleConstants.white;
89
+ backgroundColor = bg;
90
+ borderColor = bg;
91
+ } else {
92
+ borderColor = Color.offBlack64;
93
+ color = Color.offBlack64;
94
+ }
95
+
96
+ // define shape
97
+ let borderRadius;
98
+ if (multipleSelect) {
99
+ borderRadius = 3;
100
+ } else {
101
+ borderRadius = LIBRARY_ICON_SIZE;
102
+ }
103
+
104
+ return {backgroundColor, borderColor, color, borderRadius};
105
+ }
106
+
107
+ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
108
+ const {
109
+ checked,
110
+ crossedOut,
111
+ showCorrectness,
112
+ correct,
113
+ focused,
114
+ hovered,
115
+ multipleSelect,
116
+ primaryProductColor,
117
+ previouslyAnswered,
118
+ letter,
119
+ pressed,
120
+ transparentBackground,
121
+ } = props;
122
+
123
+ const dynamicStyles = getDynamicStyles(
124
+ checked,
125
+ showCorrectness,
126
+ pressed,
127
+ multipleSelect,
128
+ primaryProductColor,
129
+ correct,
130
+ transparentBackground,
131
+ );
132
+
133
+ return (
134
+ <div className={css(sharedStyles.iconWrapper)}>
135
+ <FocusRing
136
+ color={primaryProductColor}
137
+ visible={focused || hovered}
138
+ multipleSelect={multipleSelect}
139
+ >
140
+ <div
141
+ style={dynamicStyles}
142
+ data-test-id="choice-icon__library-choice-icon"
143
+ className={css(
144
+ styles.circle,
145
+ showCorrectness && correct && styles.circleCorrect,
146
+ showCorrectness && !correct && styles.circleIncorrect,
147
+ showCorrectness &&
148
+ !correct &&
149
+ (checked || previouslyAnswered) &&
150
+ styles.circleIncorrectAnswered,
151
+ )}
152
+ // used in BaseRadio to check if we actually clicked on the
153
+ // radio icon
154
+ data-is-radio-icon={true}
155
+ >
156
+ <ChoiceInner
157
+ letter={letter}
158
+ showCorrectness={showCorrectness}
159
+ correct={correct}
160
+ />
161
+ </div>
162
+ </FocusRing>
163
+ {crossedOut && <CrossOutLine color={dynamicStyles.borderColor} />}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ const styles = StyleSheet.create({
169
+ circle: {
170
+ // Make the circle
171
+ width: LIBRARY_ICON_SIZE,
172
+ height: LIBRARY_ICON_SIZE,
173
+ boxSizing: "border-box",
174
+ borderStyle: "solid",
175
+ borderWidth: 2,
176
+
177
+ // The default icons have letters in them. Style those letters.
178
+ fontFamily: styleConstants.baseFontFamily,
179
+ // NOTE(emily): We explicitly set the font weight instead of using the
180
+ // "bold font family" so that characters which fall back to the default
181
+ // font get bolded too.
182
+ fontWeight: "bold",
183
+ fontSize: 12,
184
+
185
+ // Center the contents of the icon.
186
+ display: "flex",
187
+ alignItems: "center",
188
+ justifyContent: "center",
189
+ // HACK(emily): I don't know why adding this line height makes the text
190
+ // appear centered better than any other value, but it does. In
191
+ // particular, at large zoom levels this line height does almost
192
+ // nothing, but at the default size this shifts the letter down one
193
+ // pixel so it is much better centered.
194
+ lineHeight: "1px",
195
+ },
196
+
197
+ circleCorrect: {
198
+ fontSize: LIBRARY_ICON_SIZE,
199
+ },
200
+
201
+ circleIncorrect: {
202
+ fontSize: LIBRARY_ICON_SIZE,
203
+ borderColor: styleConstants.gray68,
204
+ color: styleConstants.gray68,
205
+ },
206
+
207
+ circleIncorrectAnswered: {
208
+ backgroundColor: Color.red,
209
+ borderColor: Color.red,
210
+ color: Color.white,
211
+ },
212
+ });
213
+
214
+ export default LibraryChoiceIcon;
@@ -0,0 +1,120 @@
1
+ // @flow
2
+ /* Component for rendering a letter icon in a SAT radio choice */
3
+
4
+ import {StyleSheet, css} from "aphrodite";
5
+ import * as React from "react";
6
+
7
+ import * as styleConstants from "../../../styles/constants.js";
8
+
9
+ import CrossOutLine from "./cross-out-line.jsx";
10
+ import sharedStyles, {SAT_ICON_SIZE} from "./shared-styles.js";
11
+
12
+ type SATChoiceIconProps = {
13
+ letter: string,
14
+ checked: boolean,
15
+ multipleSelect: boolean,
16
+ correct: boolean,
17
+ reviewMode: boolean,
18
+ crossedOut: boolean,
19
+ };
20
+
21
+ // TODO(amy): figure out a better scheme for specifying these
22
+ // styles that isn't such a pain to grok. See some neat ideas
23
+ // from MDR in https://phabricator.khanacademy.org/D35249.
24
+ function constructSATStyles(
25
+ reviewMode: boolean,
26
+ correct: boolean,
27
+ checked: boolean,
28
+ multipleSelect: boolean,
29
+ ): {
30
+ color: string,
31
+ backgroundColor: ?string,
32
+ borderColor: string,
33
+ borderRadius: number,
34
+ } {
35
+ let backgroundColor;
36
+ let borderColor = styleConstants.satBlue;
37
+ let color = styleConstants.satBlue;
38
+ if (reviewMode) {
39
+ if (correct) {
40
+ borderColor = styleConstants.satCorrectColor;
41
+ color = checked
42
+ ? styleConstants.white
43
+ : styleConstants.satCorrectColor;
44
+ backgroundColor = checked
45
+ ? styleConstants.satCorrectColor
46
+ : styleConstants.white;
47
+ } else if (checked) {
48
+ borderColor = styleConstants.satIncorrectColor;
49
+ color = styleConstants.white;
50
+ backgroundColor = styleConstants.satIncorrectColor;
51
+ }
52
+ } else if (checked) {
53
+ color = styleConstants.white;
54
+ backgroundColor = styleConstants.satBlue;
55
+ }
56
+
57
+ let borderRadius;
58
+ if (multipleSelect) {
59
+ borderRadius = 3;
60
+ } else {
61
+ borderRadius = SAT_ICON_SIZE;
62
+ }
63
+
64
+ return {color, backgroundColor, borderColor, borderRadius};
65
+ }
66
+
67
+ function SATChoiceIcon(props: SATChoiceIconProps): React.Node {
68
+ const {letter, crossedOut, reviewMode, correct, checked, multipleSelect} =
69
+ props;
70
+
71
+ const {color, backgroundColor, borderColor, borderRadius} =
72
+ constructSATStyles(reviewMode, correct, checked, multipleSelect);
73
+
74
+ return (
75
+ <div className={css(sharedStyles.iconWrapper)}>
76
+ <div
77
+ className={css(styles.satCircle)}
78
+ data-test-id="choice-icon__sat-choice-icon"
79
+ style={{backgroundColor, borderColor, borderRadius}}
80
+ />
81
+ <div style={{color}} className={css(styles.letter)}>
82
+ <span>{letter}</span>
83
+ </div>
84
+ {crossedOut && <CrossOutLine color={borderColor} sat={true} />}
85
+ </div>
86
+ );
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ satCircle: {
91
+ display: "block",
92
+ borderStyle: "solid",
93
+ borderWidth: 2,
94
+ content: `''`,
95
+ height: SAT_ICON_SIZE,
96
+ width: SAT_ICON_SIZE,
97
+ top: 1,
98
+ left: 1,
99
+ },
100
+ letter: {
101
+ // These properties make sure that this element has the exact
102
+ // same size as `circle` so that we can center things
103
+ // inside of it.
104
+ border: "2px solid transparent",
105
+ width: SAT_ICON_SIZE,
106
+ height: SAT_ICON_SIZE,
107
+ position: "absolute",
108
+ top: 1,
109
+
110
+ // Center contained items.
111
+ display: "flex",
112
+ alignItems: "center",
113
+ justifyContent: "center",
114
+
115
+ fontFamily: styleConstants.boldFontFamily,
116
+ fontSize: 13,
117
+ },
118
+ });
119
+
120
+ export default SATChoiceIcon;
@@ -0,0 +1,17 @@
1
+ // @flow
2
+ import {StyleSheet} from "aphrodite";
3
+
4
+ import type {StyleDeclaration} from "aphrodite";
5
+
6
+ const SAT_ICON_SIZE = 25;
7
+ const LIBRARY_ICON_SIZE = 24;
8
+
9
+ const styles: StyleDeclaration = StyleSheet.create({
10
+ iconWrapper: {
11
+ display: "inline-block",
12
+ position: "relative",
13
+ },
14
+ });
15
+
16
+ export {SAT_ICON_SIZE, LIBRARY_ICON_SIZE};
17
+ export default styles;
@@ -1,54 +1,63 @@
1
1
  // @flow
2
2
 
3
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
4
- import classNames from "classnames";
5
- import PropTypes from "prop-types";
6
4
  import * as React from "react";
7
5
 
8
6
  import Renderer from "../../renderer.jsx";
9
7
 
10
8
  import Choice from "./choice.jsx";
11
9
 
12
- class ChoiceNoneAbove extends React.Component<$FlowFixMe> {
13
- _choice: $FlowFixMe;
14
- static propTypes = {
15
- className: PropTypes.string,
16
- content: PropTypes.node,
17
- showContent: PropTypes.bool,
10
+ import type {ChoiceProps} from "./choice.jsx";
11
+
12
+ type Props = {|
13
+ ...ChoiceProps,
14
+ showContent: boolean,
15
+ |};
16
+
17
+ type WithForwardRef = {|forwardedRef: React.Ref<"button">|};
18
+
19
+ type PropsWithForwardRef = {|
20
+ ...Props,
21
+ ...WithForwardRef,
22
+ |};
23
+
24
+ function ChoiceNoneAbove(props: PropsWithForwardRef): React.Node {
25
+ const {showContent, content, forwardedRef, ...rest} = props;
26
+
27
+ const choiceProps = {
28
+ ...rest,
29
+ content: showContent ? (
30
+ content
31
+ ) : (
32
+ // We use a Renderer here because that is how
33
+ // `this.props.content` is wrapped otherwise.
34
+ // We pass in a key here so that we avoid a semi-spurious
35
+ // react warning when we render this in the same place
36
+ // as the previous choice content renderer.
37
+ // Note this destroys state, but since all we're doing
38
+ // is outputting "None of the above", that is okay.
39
+ //
40
+ // todo(matthewc): this seems like way overkill
41
+ // just to render a string
42
+ <Renderer
43
+ key="noneOfTheAboveRenderer"
44
+ content={i18n._("None of the above")}
45
+ />
46
+ ),
18
47
  };
19
48
 
20
- static defaultProps: $FlowFixMe = {
21
- showContent: true,
22
- };
49
+ return <Choice {...choiceProps} ref={forwardedRef} />;
50
+ }
23
51
 
24
- focusInput: () => void = () => {
25
- this._choice.focusInput();
26
- };
52
+ ChoiceNoneAbove.defaultProps = {
53
+ showContent: true,
54
+ };
27
55
 
28
- render(): React.Node {
29
- const choiceProps = {
30
- ...this.props,
31
- className: classNames(this.props.className, "none-of-above"),
32
- content: this.props.showContent ? (
33
- this.props.content
34
- ) : (
35
- // We use a Renderer here because that is how
36
- // `this.props.content` is wrapped otherwise.
37
- // We pass in a key here so that we avoid a semi-spurious
38
- // react warning when we render this in the same place
39
- // as the previous choice content renderer.
40
- // Note this destroys state, but since all we're doing
41
- // is outputting "None of the above", that is okay.
42
- <Renderer
43
- key="noneOfTheAboveRenderer"
44
- content={i18n._("None of the above")}
45
- />
46
- ),
47
- ref: (el) => (this._choice = el),
48
- };
49
-
50
- return <Choice {...choiceProps} />;
51
- }
52
- }
56
+ type ExportProps = $Diff<
57
+ React.ElementConfig<typeof ChoiceNoneAbove>,
58
+ WithForwardRef,
59
+ >;
53
60
 
54
- export default ChoiceNoneAbove;
61
+ export default (React.forwardRef<ExportProps, HTMLButtonElement>(
62
+ (props, ref) => <ChoiceNoneAbove {...props} forwardedRef={ref} />,
63
+ ): React.AbstractComponent<ExportProps, HTMLButtonElement>);