@khanacademy/perseus 0.4.3 → 1.0.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 (50) hide show
  1. package/.eslintrc.js +6 -1
  2. package/CHANGELOG.md +34 -0
  3. package/dist/es/index.js +1078 -1299
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.js +1077 -1304
  6. package/dist/index.js.map +1 -1
  7. package/dist/strings.js +0 -4
  8. package/package.json +9 -8
  9. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +110 -110
  10. package/src/perseus-api.jsx +10 -36
  11. package/src/perseus-types.js +0 -138
  12. package/src/styles/constants.js +0 -2
  13. package/src/styles/variables.less +0 -2
  14. package/src/types.js +18 -35
  15. package/src/util.js +0 -6
  16. package/src/version.js +1 -1
  17. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  18. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +110 -110
  19. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +1136 -1160
  20. package/src/widgets/__tests__/radio/base-radio_test.jsx +243 -41
  21. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  22. package/src/widgets/__tests__/radio/choice_test.jsx +201 -0
  23. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  24. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  25. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  26. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  27. package/src/widgets/passage/passage-markdown.jsx +176 -120
  28. package/src/widgets/passage.jsx +48 -46
  29. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  30. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  31. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  32. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  33. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  34. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  35. package/src/widgets/radio/base-radio.jsx +483 -556
  36. package/src/widgets/radio/choice-icon/choice-icon.jsx +155 -0
  37. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  38. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +219 -0
  39. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +129 -0
  40. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  41. package/src/widgets/radio/choice-none-above.jsx +49 -40
  42. package/src/widgets/radio/choice.jsx +282 -353
  43. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  44. package/src/widgets/radio/focus-ring.jsx +29 -22
  45. package/src/widgets/radio/option-status.jsx +58 -36
  46. package/src/widgets/radio/widget.jsx +181 -136
  47. package/src/widgets/radio.jsx +16 -17
  48. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  49. package/src/widgets/radio/choice-icon.jsx +0 -469
  50. package/src/widgets/radio/cross-out-button.jsx +0 -134
@@ -0,0 +1,155 @@
1
+ // @flow
2
+ /* Component for rendering a letter icon in radio choice */
3
+
4
+ import Color from "@khanacademy/wonder-blocks-color";
5
+ import * as i18n from "@khanacademy/wonder-blocks-i18n";
6
+ import * as React from "react";
7
+
8
+ import {getChoiceLetter} from "../util.js";
9
+
10
+ import LibraryChoiceIcon from "./library-choice-icon.jsx";
11
+ import SATChoiceIcon from "./sat-choice-icon.jsx";
12
+
13
+ type ChoiceIconProps = {|
14
+ pos: number,
15
+ checked: boolean,
16
+ crossedOut: boolean,
17
+ focused: boolean,
18
+ hovered: boolean,
19
+ pressed: boolean,
20
+ correct: boolean,
21
+ showCorrectness: boolean,
22
+ // TODO(amy): if we go this "product" flag route, define this type
23
+ // somewhere shared
24
+ product: "sat" | "library",
25
+ multipleSelect: boolean,
26
+ reviewMode: boolean,
27
+ previouslyAnswered: boolean,
28
+ // TODO(mdr): The CrossOutButton needs a transparent-background ChoiceIcon,
29
+ // so I've added this prop. I'm not sure why we have backgrounds in the
30
+ // general case, though? When does the choice container have a
31
+ // non-white background, aside from SAT, which uses a different icon?
32
+ transparentBackground?: boolean,
33
+
34
+ primaryProductColor?: string,
35
+ |};
36
+
37
+ function getA11yText(
38
+ letter: string,
39
+ checked: boolean,
40
+ correct: boolean,
41
+ crossedOut: boolean,
42
+ showCorrectness: boolean,
43
+ ): string {
44
+ // There are two pieces of metadata we want to add to each a11yText:
45
+ // whether the answer was checked/crossed-out/neither, and whether the
46
+ // answer is correct/incorrect/not-yet-revealed.
47
+ //
48
+ // Translation is tricky for cross-product situations like this, so
49
+ // we've just enumerated all 9 possibilities as separate strings.
50
+ if (showCorrectness && correct) {
51
+ if (checked) {
52
+ return i18n._("(Choice %(letter)s, Checked, Correct)", {
53
+ letter,
54
+ });
55
+ }
56
+ if (crossedOut) {
57
+ return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
58
+ letter,
59
+ });
60
+ }
61
+ return i18n._("(Choice %(letter)s, Correct)", {
62
+ letter,
63
+ });
64
+ }
65
+ if (showCorrectness && !correct) {
66
+ if (checked) {
67
+ return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
68
+ letter,
69
+ });
70
+ }
71
+ if (crossedOut) {
72
+ return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
73
+ letter,
74
+ });
75
+ }
76
+ return i18n._("(Choice %(letter)s, Incorrect)", {
77
+ letter,
78
+ });
79
+ }
80
+ if (checked) {
81
+ return i18n._("(Choice %(letter)s, Checked)", {
82
+ letter,
83
+ });
84
+ }
85
+ if (crossedOut) {
86
+ return i18n._("(Choice %(letter)s, Crossed out)", {
87
+ letter,
88
+ });
89
+ }
90
+ return i18n._("(Choice %(letter)s)", {
91
+ letter,
92
+ });
93
+ }
94
+
95
+ function ChoiceIcon(props: ChoiceIconProps): React.Node {
96
+ const {
97
+ pos,
98
+ reviewMode,
99
+ checked,
100
+ crossedOut,
101
+ correct,
102
+ product,
103
+ multipleSelect,
104
+ showCorrectness,
105
+ focused,
106
+ hovered,
107
+ pressed,
108
+ primaryProductColor = Color.blue,
109
+ previouslyAnswered,
110
+ transparentBackground,
111
+ } = props;
112
+
113
+ const letter = getChoiceLetter(pos);
114
+ const a11yText = getA11yText(
115
+ letter,
116
+ checked,
117
+ correct,
118
+ crossedOut,
119
+ showCorrectness,
120
+ );
121
+
122
+ if (product === "sat") {
123
+ return (
124
+ <SATChoiceIcon
125
+ letter={letter}
126
+ a11yText={a11yText}
127
+ reviewMode={reviewMode}
128
+ checked={checked}
129
+ correct={correct}
130
+ crossedOut={crossedOut}
131
+ multipleSelect={multipleSelect}
132
+ />
133
+ );
134
+ }
135
+ return (
136
+ <LibraryChoiceIcon
137
+ letter={letter}
138
+ a11yText={a11yText}
139
+ reviewMode={reviewMode}
140
+ checked={checked}
141
+ crossedOut={crossedOut}
142
+ focused={focused}
143
+ hovered={hovered}
144
+ pressed={pressed}
145
+ correct={correct}
146
+ showCorrectness={showCorrectness}
147
+ primaryProductColor={primaryProductColor}
148
+ previouslyAnswered={previouslyAnswered}
149
+ transparentBackground={transparentBackground}
150
+ multipleSelect={multipleSelect}
151
+ />
152
+ );
153
+ }
154
+
155
+ 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,219 @@
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
+ a11yText: string,
19
+ checked: boolean,
20
+ crossedOut: boolean,
21
+ focused: boolean,
22
+ hovered: boolean,
23
+ pressed: boolean,
24
+ multipleSelect: boolean,
25
+ correct: ?boolean,
26
+ reviewMode: boolean,
27
+ showCorrectness: boolean,
28
+ primaryProductColor: string,
29
+ previouslyAnswered: boolean,
30
+ transparentBackground?: boolean,
31
+ };
32
+
33
+ function ChoiceInner(props: {
34
+ letter: string,
35
+ showCorrectness: boolean,
36
+ correct: ?boolean,
37
+ }) {
38
+ const {letter, showCorrectness, correct} = props;
39
+
40
+ if (!showCorrectness) {
41
+ return letter;
42
+ }
43
+ if (correct) {
44
+ return (
45
+ <InlineIcon
46
+ {...iconCheck}
47
+ style={{
48
+ position: "relative",
49
+ top: -1,
50
+ }}
51
+ />
52
+ );
53
+ }
54
+ return <InlineIcon {...iconMinus} />;
55
+ }
56
+
57
+ // Handle dynamic styling of the multiple choice icon. Most
58
+ // MC icon styles are constant, but we do allow the caller
59
+ // to specify the selected color, and thus must control styles
60
+ // related to the selected state dynamically.
61
+ function getDynamicStyles(
62
+ checked: boolean,
63
+ showCorrectness: boolean,
64
+ pressed: boolean,
65
+ multipleSelect: boolean,
66
+ primaryProductColor: string,
67
+ correct: ?boolean,
68
+ transparentBackground?: boolean,
69
+ ): {
70
+ backgroundColor: ?string,
71
+ borderColor: string,
72
+ color: string,
73
+ borderRadius: number,
74
+ } {
75
+ let backgroundColor;
76
+ let borderColor;
77
+ let color;
78
+ if (!showCorrectness && pressed) {
79
+ borderColor = primaryProductColor;
80
+ color = primaryProductColor;
81
+ backgroundColor = transparentBackground
82
+ ? "transparent"
83
+ : styleConstants.white;
84
+ } else if (checked) {
85
+ // Note: kaGreen is not only the default product color,
86
+ // but also the "correctness" color
87
+ const bg =
88
+ showCorrectness && correct ? Color.green : primaryProductColor;
89
+ color = styleConstants.white;
90
+ backgroundColor = bg;
91
+ borderColor = bg;
92
+ } else {
93
+ borderColor = Color.offBlack64;
94
+ color = Color.offBlack64;
95
+ }
96
+
97
+ // define shape
98
+ let borderRadius;
99
+ if (multipleSelect) {
100
+ borderRadius = 3;
101
+ } else {
102
+ borderRadius = LIBRARY_ICON_SIZE;
103
+ }
104
+
105
+ return {backgroundColor, borderColor, color, borderRadius};
106
+ }
107
+
108
+ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
109
+ const {
110
+ a11yText,
111
+ checked,
112
+ crossedOut,
113
+ showCorrectness,
114
+ correct,
115
+ focused,
116
+ hovered,
117
+ multipleSelect,
118
+ primaryProductColor,
119
+ previouslyAnswered,
120
+ letter,
121
+ pressed,
122
+ transparentBackground,
123
+ } = props;
124
+
125
+ const dynamicStyles = getDynamicStyles(
126
+ checked,
127
+ showCorrectness,
128
+ pressed,
129
+ multipleSelect,
130
+ primaryProductColor,
131
+ correct,
132
+ transparentBackground,
133
+ );
134
+
135
+ return (
136
+ <div className={css(sharedStyles.iconWrapper)}>
137
+ <FocusRing
138
+ color={primaryProductColor}
139
+ visible={focused || hovered}
140
+ multipleSelect={multipleSelect}
141
+ >
142
+ <div
143
+ style={dynamicStyles}
144
+ data-test-id="choice-icon__library-choice-icon"
145
+ className={css(
146
+ styles.circle,
147
+ showCorrectness && correct && styles.circleCorrect,
148
+ showCorrectness && !correct && styles.circleIncorrect,
149
+ showCorrectness &&
150
+ !correct &&
151
+ (checked || previouslyAnswered) &&
152
+ styles.circleIncorrectAnswered,
153
+ )}
154
+ // used in BaseRadio to check if we actually clicked on the
155
+ // radio icon
156
+ data-is-radio-icon={true}
157
+ >
158
+ <div className="perseus-sr-only">{a11yText}</div>
159
+ <div aria-hidden>
160
+ <ChoiceInner
161
+ letter={letter}
162
+ showCorrectness={showCorrectness}
163
+ correct={correct}
164
+ />
165
+ </div>
166
+ </div>
167
+ </FocusRing>
168
+ {crossedOut && <CrossOutLine color={dynamicStyles.borderColor} />}
169
+ </div>
170
+ );
171
+ }
172
+
173
+ const styles = StyleSheet.create({
174
+ circle: {
175
+ // Make the circle
176
+ width: LIBRARY_ICON_SIZE,
177
+ height: LIBRARY_ICON_SIZE,
178
+ boxSizing: "border-box",
179
+ borderStyle: "solid",
180
+ borderWidth: 2,
181
+
182
+ // The default icons have letters in them. Style those letters.
183
+ fontFamily: styleConstants.baseFontFamily,
184
+ // NOTE(emily): We explicitly set the font weight instead of using the
185
+ // "bold font family" so that characters which fall back to the default
186
+ // font get bolded too.
187
+ fontWeight: "bold",
188
+ fontSize: 12,
189
+
190
+ // Center the contents of the icon.
191
+ display: "flex",
192
+ alignItems: "center",
193
+ justifyContent: "center",
194
+ // HACK(emily): I don't know why adding this line height makes the text
195
+ // appear centered better than any other value, but it does. In
196
+ // particular, at large zoom levels this line height does almost
197
+ // nothing, but at the default size this shifts the letter down one
198
+ // pixel so it is much better centered.
199
+ lineHeight: "1px",
200
+ },
201
+
202
+ circleCorrect: {
203
+ fontSize: LIBRARY_ICON_SIZE,
204
+ },
205
+
206
+ circleIncorrect: {
207
+ fontSize: LIBRARY_ICON_SIZE,
208
+ borderColor: styleConstants.gray68,
209
+ color: styleConstants.gray68,
210
+ },
211
+
212
+ circleIncorrectAnswered: {
213
+ backgroundColor: Color.red,
214
+ borderColor: Color.red,
215
+ color: Color.white,
216
+ },
217
+ });
218
+
219
+ export default LibraryChoiceIcon;
@@ -0,0 +1,129 @@
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
+ a11yText: string,
15
+ checked: boolean,
16
+ multipleSelect: boolean,
17
+ correct: boolean,
18
+ reviewMode: boolean,
19
+ crossedOut: boolean,
20
+ };
21
+
22
+ // TODO(amy): figure out a better scheme for specifying these
23
+ // styles that isn't such a pain to grok. See some neat ideas
24
+ // from MDR in https://phabricator.khanacademy.org/D35249.
25
+ function constructSATStyles(
26
+ reviewMode: boolean,
27
+ correct: boolean,
28
+ checked: boolean,
29
+ multipleSelect: boolean,
30
+ ): {
31
+ color: string,
32
+ backgroundColor: ?string,
33
+ borderColor: string,
34
+ borderRadius: number,
35
+ } {
36
+ let backgroundColor;
37
+ let borderColor = styleConstants.satBlue;
38
+ let color = styleConstants.satBlue;
39
+ if (reviewMode) {
40
+ if (correct) {
41
+ borderColor = styleConstants.satCorrectColor;
42
+ color = checked
43
+ ? styleConstants.white
44
+ : styleConstants.satCorrectColor;
45
+ backgroundColor = checked
46
+ ? styleConstants.satCorrectColor
47
+ : styleConstants.white;
48
+ } else if (checked) {
49
+ borderColor = styleConstants.satIncorrectColor;
50
+ color = styleConstants.white;
51
+ backgroundColor = styleConstants.satIncorrectColor;
52
+ }
53
+ } else if (checked) {
54
+ color = styleConstants.white;
55
+ backgroundColor = styleConstants.satBlue;
56
+ }
57
+
58
+ let borderRadius;
59
+ if (multipleSelect) {
60
+ borderRadius = 3;
61
+ } else {
62
+ borderRadius = SAT_ICON_SIZE;
63
+ }
64
+
65
+ return {color, backgroundColor, borderColor, borderRadius};
66
+ }
67
+
68
+ function SATChoiceIcon(props: SATChoiceIconProps): React.Node {
69
+ const {
70
+ letter,
71
+ a11yText,
72
+ crossedOut,
73
+ reviewMode,
74
+ correct,
75
+ checked,
76
+ multipleSelect,
77
+ } = props;
78
+
79
+ const {color, backgroundColor, borderColor, borderRadius} =
80
+ constructSATStyles(reviewMode, correct, checked, multipleSelect);
81
+
82
+ return (
83
+ <div className={css(sharedStyles.iconWrapper)}>
84
+ <div
85
+ className={css(styles.satCircle)}
86
+ data-test-id="choice-icon__sat-choice-icon"
87
+ style={{backgroundColor, borderColor, borderRadius}}
88
+ />
89
+ <div style={{color}} className={css(styles.letter)}>
90
+ <span className="perseus-sr-only">{a11yText}</span>
91
+ <span aria-hidden="true">{letter}</span>
92
+ </div>
93
+ {crossedOut && <CrossOutLine color={borderColor} sat={true} />}
94
+ </div>
95
+ );
96
+ }
97
+
98
+ const styles = StyleSheet.create({
99
+ satCircle: {
100
+ display: "block",
101
+ borderStyle: "solid",
102
+ borderWidth: 2,
103
+ content: `''`,
104
+ height: SAT_ICON_SIZE,
105
+ width: SAT_ICON_SIZE,
106
+ top: 1,
107
+ left: 1,
108
+ },
109
+ letter: {
110
+ // These properties make sure that this element has the exact
111
+ // same size as `circle` so that we can center things
112
+ // inside of it.
113
+ border: "2px solid transparent",
114
+ width: SAT_ICON_SIZE,
115
+ height: SAT_ICON_SIZE,
116
+ position: "absolute",
117
+ top: 1,
118
+
119
+ // Center contained items.
120
+ display: "flex",
121
+ alignItems: "center",
122
+ justifyContent: "center",
123
+
124
+ fontFamily: styleConstants.boldFontFamily,
125
+ fontSize: 13,
126
+ },
127
+ });
128
+
129
+ 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>);