@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.
- package/.eslintrc.js +6 -1
- package/CHANGELOG.md +34 -0
- package/dist/es/index.js +1078 -1299
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +1077 -1304
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -4
- package/package.json +9 -8
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +110 -110
- package/src/perseus-api.jsx +10 -36
- package/src/perseus-types.js +0 -138
- package/src/styles/constants.js +0 -2
- package/src/styles/variables.less +0 -2
- package/src/types.js +18 -35
- package/src/util.js +0 -6
- package/src/version.js +1 -1
- package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +110 -110
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +1136 -1160
- package/src/widgets/__tests__/radio/base-radio_test.jsx +243 -41
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +201 -0
- package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
- package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
- package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
- package/src/widgets/passage/get-line-height-for-node.js +4 -5
- package/src/widgets/passage/passage-markdown.jsx +176 -120
- package/src/widgets/passage.jsx +48 -46
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
- package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
- package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
- package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
- package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
- package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
- package/src/widgets/radio/base-radio.jsx +483 -556
- package/src/widgets/radio/choice-icon/choice-icon.jsx +155 -0
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +219 -0
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +129 -0
- package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
- package/src/widgets/radio/choice-none-above.jsx +49 -40
- package/src/widgets/radio/choice.jsx +282 -353
- package/src/widgets/radio/choice.jsx.fixture.js +0 -18
- package/src/widgets/radio/focus-ring.jsx +29 -22
- package/src/widgets/radio/option-status.jsx +58 -36
- package/src/widgets/radio/widget.jsx +181 -136
- package/src/widgets/radio.jsx +16 -17
- package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
- package/src/widgets/radio/choice-icon.jsx +0 -469
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
};
|
|
49
|
+
return <Choice {...choiceProps} ref={forwardedRef} />;
|
|
50
|
+
}
|
|
23
51
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
52
|
+
ChoiceNoneAbove.defaultProps = {
|
|
53
|
+
showContent: true,
|
|
54
|
+
};
|
|
27
55
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
61
|
+
export default (React.forwardRef<ExportProps, HTMLButtonElement>(
|
|
62
|
+
(props, ref) => <ChoiceNoneAbove {...props} forwardedRef={ref} />,
|
|
63
|
+
): React.AbstractComponent<ExportProps, HTMLButtonElement>);
|