@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
|
@@ -1,469 +0,0 @@
|
|
|
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 {StyleSheet, css} from "aphrodite";
|
|
7
|
-
import * as React from "react";
|
|
8
|
-
|
|
9
|
-
import InlineIcon from "../../components/inline-icon.jsx";
|
|
10
|
-
import {iconCheck, iconMinus} from "../../icon-paths.js";
|
|
11
|
-
import * as styleConstants from "../../styles/constants.js";
|
|
12
|
-
|
|
13
|
-
import FocusRing from "./focus-ring.jsx";
|
|
14
|
-
import {getChoiceLetter} from "./util.js";
|
|
15
|
-
|
|
16
|
-
const SAT_ICON_SIZE = 25;
|
|
17
|
-
class SATChoiceIcon extends React.Component<{
|
|
18
|
-
letter: string,
|
|
19
|
-
a11yText: string,
|
|
20
|
-
checked: boolean,
|
|
21
|
-
correct: boolean,
|
|
22
|
-
reviewMode: boolean,
|
|
23
|
-
crossedOut: boolean,
|
|
24
|
-
...
|
|
25
|
-
}> {
|
|
26
|
-
// TODO(amy): figure out a better scheme for specifying these
|
|
27
|
-
// styles that isn't such a pain to grok. See some neat ideas
|
|
28
|
-
// from MDR in https://phabricator.khanacademy.org/D35249.
|
|
29
|
-
constructStyles(
|
|
30
|
-
reviewMode: boolean,
|
|
31
|
-
correct: boolean,
|
|
32
|
-
checked: boolean,
|
|
33
|
-
): {
|
|
34
|
-
color: string,
|
|
35
|
-
backgroundColor: ?string,
|
|
36
|
-
borderColor: string,
|
|
37
|
-
...
|
|
38
|
-
} {
|
|
39
|
-
let backgroundColor;
|
|
40
|
-
let borderColor = styleConstants.satBlue;
|
|
41
|
-
let color = styleConstants.satBlue;
|
|
42
|
-
if (reviewMode) {
|
|
43
|
-
if (correct) {
|
|
44
|
-
borderColor = styleConstants.satCorrectColor;
|
|
45
|
-
color = checked
|
|
46
|
-
? styleConstants.white
|
|
47
|
-
: styleConstants.satCorrectColor;
|
|
48
|
-
backgroundColor = checked
|
|
49
|
-
? styleConstants.satCorrectColor
|
|
50
|
-
: styleConstants.white;
|
|
51
|
-
} else if (checked) {
|
|
52
|
-
borderColor = styleConstants.satIncorrectColor;
|
|
53
|
-
color = styleConstants.white;
|
|
54
|
-
backgroundColor = styleConstants.satIncorrectColor;
|
|
55
|
-
}
|
|
56
|
-
} else if (checked) {
|
|
57
|
-
color = styleConstants.white;
|
|
58
|
-
backgroundColor = styleConstants.satBlue;
|
|
59
|
-
}
|
|
60
|
-
return {color, backgroundColor, borderColor};
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
render(): React.Node {
|
|
64
|
-
const {letter, a11yText, reviewMode, checked, correct, crossedOut} =
|
|
65
|
-
this.props;
|
|
66
|
-
const {color, backgroundColor, borderColor} = this.constructStyles(
|
|
67
|
-
reviewMode,
|
|
68
|
-
correct,
|
|
69
|
-
checked,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
return (
|
|
73
|
-
<div className={css(styles.iconWrapper)}>
|
|
74
|
-
<div
|
|
75
|
-
className={css(styles.satCircle)}
|
|
76
|
-
style={{backgroundColor, borderColor}}
|
|
77
|
-
/>
|
|
78
|
-
<div style={{color}} className={css(styles.letter)}>
|
|
79
|
-
<span className="perseus-sr-only">{a11yText}</span>
|
|
80
|
-
<span aria-hidden="true">{letter}</span>
|
|
81
|
-
</div>
|
|
82
|
-
{crossedOut && <CrossOutLine color={borderColor} sat={true} />}
|
|
83
|
-
</div>
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const LIBRARY_ICON_SIZE = 24;
|
|
89
|
-
|
|
90
|
-
class LibraryChoiceIcon extends React.Component<{
|
|
91
|
-
letter: string,
|
|
92
|
-
a11yText: string,
|
|
93
|
-
checked: boolean,
|
|
94
|
-
crossedOut: boolean,
|
|
95
|
-
pressed: boolean,
|
|
96
|
-
focused: boolean,
|
|
97
|
-
correct: ?boolean,
|
|
98
|
-
reviewMode: boolean,
|
|
99
|
-
showCorrectness: boolean,
|
|
100
|
-
primaryProductColor: string,
|
|
101
|
-
previouslyAnswered: boolean,
|
|
102
|
-
transparentBackground?: boolean,
|
|
103
|
-
...
|
|
104
|
-
}> {
|
|
105
|
-
getChoiceInner() {
|
|
106
|
-
const {letter, showCorrectness, correct} = this.props;
|
|
107
|
-
|
|
108
|
-
if (!showCorrectness) {
|
|
109
|
-
return letter;
|
|
110
|
-
}
|
|
111
|
-
if (correct) {
|
|
112
|
-
return (
|
|
113
|
-
<InlineIcon
|
|
114
|
-
{...iconCheck}
|
|
115
|
-
style={{
|
|
116
|
-
position: "relative",
|
|
117
|
-
top: -1,
|
|
118
|
-
}}
|
|
119
|
-
/>
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
return <InlineIcon {...iconMinus} />;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Handle dynamic styling of the multiple choice icon. Most
|
|
126
|
-
// MC icon styles are constant, but we do allow the caller
|
|
127
|
-
// to specify the selected color, and thus must control styles
|
|
128
|
-
// related to the selected state dynamically.
|
|
129
|
-
getDynamicStyles() {
|
|
130
|
-
const {
|
|
131
|
-
checked,
|
|
132
|
-
showCorrectness,
|
|
133
|
-
pressed,
|
|
134
|
-
primaryProductColor,
|
|
135
|
-
correct,
|
|
136
|
-
transparentBackground,
|
|
137
|
-
} = this.props;
|
|
138
|
-
if (!showCorrectness && pressed) {
|
|
139
|
-
return {
|
|
140
|
-
borderColor: primaryProductColor,
|
|
141
|
-
color: primaryProductColor,
|
|
142
|
-
backgroundColor: transparentBackground
|
|
143
|
-
? "transparent"
|
|
144
|
-
: styleConstants.white,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
if (checked) {
|
|
148
|
-
// Note: kaGreen is not only the default product color,
|
|
149
|
-
// but also the "correctness" color
|
|
150
|
-
const bg =
|
|
151
|
-
showCorrectness && correct ? Color.green : primaryProductColor;
|
|
152
|
-
return {
|
|
153
|
-
color: styleConstants.white,
|
|
154
|
-
backgroundColor: bg,
|
|
155
|
-
borderColor: bg,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
borderColor: Color.offBlack64,
|
|
160
|
-
color: Color.offBlack64,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
render(): React.Node {
|
|
165
|
-
const {
|
|
166
|
-
a11yText,
|
|
167
|
-
checked,
|
|
168
|
-
crossedOut,
|
|
169
|
-
showCorrectness,
|
|
170
|
-
correct,
|
|
171
|
-
focused,
|
|
172
|
-
primaryProductColor,
|
|
173
|
-
previouslyAnswered,
|
|
174
|
-
} = this.props;
|
|
175
|
-
|
|
176
|
-
const dynamicStyles = this.getDynamicStyles();
|
|
177
|
-
|
|
178
|
-
return (
|
|
179
|
-
<div className={css(styles.iconWrapper)}>
|
|
180
|
-
<FocusRing color={primaryProductColor} visible={focused}>
|
|
181
|
-
<div
|
|
182
|
-
style={dynamicStyles}
|
|
183
|
-
className={css(
|
|
184
|
-
styles.libraryCircle,
|
|
185
|
-
showCorrectness &&
|
|
186
|
-
correct &&
|
|
187
|
-
styles.libraryCircleCorrect,
|
|
188
|
-
showCorrectness &&
|
|
189
|
-
!correct &&
|
|
190
|
-
styles.libraryCircleIncorrect,
|
|
191
|
-
showCorrectness &&
|
|
192
|
-
!correct &&
|
|
193
|
-
(checked || previouslyAnswered) &&
|
|
194
|
-
styles.libraryCircleIncorrectAnswered,
|
|
195
|
-
)}
|
|
196
|
-
// used in BaseRadio to check if we actually clicked on the
|
|
197
|
-
// radio icon
|
|
198
|
-
data-is-radio-icon={true}
|
|
199
|
-
>
|
|
200
|
-
<div className="perseus-sr-only">{a11yText}</div>
|
|
201
|
-
<div aria-hidden>{this.getChoiceInner()}</div>
|
|
202
|
-
</div>
|
|
203
|
-
</FocusRing>
|
|
204
|
-
{crossedOut && (
|
|
205
|
-
<CrossOutLine color={dynamicStyles.borderColor} />
|
|
206
|
-
)}
|
|
207
|
-
</div>
|
|
208
|
-
);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// The "cross-out line" SVG is a bit bigger than the library icon, to provide
|
|
213
|
-
// extra space for the cute rounded line cap.
|
|
214
|
-
const CROSS_OUT_LINE_PADDING = 2;
|
|
215
|
-
const CROSS_OUT_LINE_SIZE = LIBRARY_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
|
|
216
|
-
const SAT_CROSS_OUT_LINE_SIZE = SAT_ICON_SIZE + CROSS_OUT_LINE_PADDING * 2;
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* The "cross-out line" that appears over the icon when the choice has been
|
|
220
|
-
* `crossedOut`.
|
|
221
|
-
*/
|
|
222
|
-
function CrossOutLine(props: {color: string, sat?: boolean, ...}) {
|
|
223
|
-
const crossOutLineSize = props.sat
|
|
224
|
-
? SAT_CROSS_OUT_LINE_SIZE
|
|
225
|
-
: CROSS_OUT_LINE_SIZE;
|
|
226
|
-
return (
|
|
227
|
-
<svg
|
|
228
|
-
width={crossOutLineSize}
|
|
229
|
-
height={crossOutLineSize}
|
|
230
|
-
viewBox={`0 0 ${crossOutLineSize} ${crossOutLineSize}`}
|
|
231
|
-
className={css(styles.crossOutLine)}
|
|
232
|
-
>
|
|
233
|
-
<line
|
|
234
|
-
// The line stretches from the bottom-left to top-right.
|
|
235
|
-
// We don't quite go to the _very_ corner, because the cute
|
|
236
|
-
// rounded line cap needs to bleed into our padding.
|
|
237
|
-
x1={CROSS_OUT_LINE_PADDING}
|
|
238
|
-
x2={crossOutLineSize - CROSS_OUT_LINE_PADDING}
|
|
239
|
-
y1={crossOutLineSize - CROSS_OUT_LINE_PADDING}
|
|
240
|
-
y2={CROSS_OUT_LINE_PADDING}
|
|
241
|
-
stroke={props.color}
|
|
242
|
-
strokeWidth="2"
|
|
243
|
-
strokeLinecap="round"
|
|
244
|
-
/>
|
|
245
|
-
</svg>
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
type ChoiceIconProps = {|
|
|
250
|
-
pos: number,
|
|
251
|
-
checked: boolean,
|
|
252
|
-
crossedOut: boolean,
|
|
253
|
-
pressed: boolean,
|
|
254
|
-
focused: boolean,
|
|
255
|
-
correct: boolean,
|
|
256
|
-
showCorrectness: boolean,
|
|
257
|
-
// TODO(amy): if we go this "product" flag route, define this type
|
|
258
|
-
// somewhere shared
|
|
259
|
-
product: "sat" | "library",
|
|
260
|
-
reviewMode: boolean,
|
|
261
|
-
previouslyAnswered: boolean,
|
|
262
|
-
// TODO(mdr): The CrossOutButton needs a transparent-background ChoiceIcon,
|
|
263
|
-
// so I've added this prop. I'm not sure why we have backgrounds in the
|
|
264
|
-
// general case, though? When does the choice container have a
|
|
265
|
-
// non-white background, aside from SAT, which uses a different icon?
|
|
266
|
-
transparentBackground?: boolean,
|
|
267
|
-
|
|
268
|
-
primaryProductColor: string,
|
|
269
|
-
|};
|
|
270
|
-
|
|
271
|
-
class ChoiceIcon extends React.Component<ChoiceIconProps> {
|
|
272
|
-
static defaultProps: {|primaryProductColor: string|} = {
|
|
273
|
-
primaryProductColor: Color.blue,
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
a11yText(letter: string): string {
|
|
277
|
-
const {checked, correct, crossedOut, showCorrectness} = this.props;
|
|
278
|
-
|
|
279
|
-
// There are two pieces of metadata we want to add to each a11yText:
|
|
280
|
-
// whether the answer was checked/crossed-out/neither, and whether the
|
|
281
|
-
// answer is correct/incorrect/not-yet-revealed.
|
|
282
|
-
//
|
|
283
|
-
// Translation is tricky for cross-product situations like this, so
|
|
284
|
-
// we've just enumerated all 9 possibilities as separate strings.
|
|
285
|
-
if (showCorrectness && correct) {
|
|
286
|
-
if (checked) {
|
|
287
|
-
return i18n._("(Choice %(letter)s, Checked, Correct)", {
|
|
288
|
-
letter,
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
if (crossedOut) {
|
|
292
|
-
return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
|
|
293
|
-
letter,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
return i18n._("(Choice %(letter)s, Correct)", {
|
|
297
|
-
letter,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
if (showCorrectness && !correct) {
|
|
301
|
-
if (checked) {
|
|
302
|
-
return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
|
|
303
|
-
letter,
|
|
304
|
-
});
|
|
305
|
-
}
|
|
306
|
-
if (crossedOut) {
|
|
307
|
-
return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
|
|
308
|
-
letter,
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
return i18n._("(Choice %(letter)s, Incorrect)", {
|
|
312
|
-
letter,
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
if (checked) {
|
|
316
|
-
return i18n._("(Choice %(letter)s, Checked)", {
|
|
317
|
-
letter,
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
if (crossedOut) {
|
|
321
|
-
return i18n._("(Choice %(letter)s, Crossed out)", {
|
|
322
|
-
letter,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
return i18n._("(Choice %(letter)s)", {
|
|
326
|
-
letter,
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
render(): React.Node {
|
|
331
|
-
const {
|
|
332
|
-
pos,
|
|
333
|
-
reviewMode,
|
|
334
|
-
checked,
|
|
335
|
-
crossedOut,
|
|
336
|
-
correct,
|
|
337
|
-
product,
|
|
338
|
-
showCorrectness,
|
|
339
|
-
pressed,
|
|
340
|
-
focused,
|
|
341
|
-
primaryProductColor,
|
|
342
|
-
previouslyAnswered,
|
|
343
|
-
transparentBackground,
|
|
344
|
-
} = this.props;
|
|
345
|
-
|
|
346
|
-
const letter = getChoiceLetter(pos);
|
|
347
|
-
|
|
348
|
-
if (product === "sat") {
|
|
349
|
-
return (
|
|
350
|
-
<SATChoiceIcon
|
|
351
|
-
letter={letter}
|
|
352
|
-
a11yText={this.a11yText(letter)}
|
|
353
|
-
reviewMode={reviewMode}
|
|
354
|
-
checked={checked}
|
|
355
|
-
correct={correct}
|
|
356
|
-
crossedOut={crossedOut}
|
|
357
|
-
/>
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
return (
|
|
361
|
-
<LibraryChoiceIcon
|
|
362
|
-
letter={letter}
|
|
363
|
-
a11yText={this.a11yText(letter)}
|
|
364
|
-
reviewMode={reviewMode}
|
|
365
|
-
checked={checked}
|
|
366
|
-
crossedOut={crossedOut}
|
|
367
|
-
pressed={pressed}
|
|
368
|
-
focused={focused}
|
|
369
|
-
correct={correct}
|
|
370
|
-
showCorrectness={showCorrectness}
|
|
371
|
-
primaryProductColor={primaryProductColor}
|
|
372
|
-
previouslyAnswered={previouslyAnswered}
|
|
373
|
-
transparentBackground={transparentBackground}
|
|
374
|
-
/>
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const styles = StyleSheet.create({
|
|
380
|
-
satCircle: {
|
|
381
|
-
display: "block",
|
|
382
|
-
borderRadius: SAT_ICON_SIZE,
|
|
383
|
-
borderStyle: "solid",
|
|
384
|
-
borderWidth: 2,
|
|
385
|
-
content: `''`,
|
|
386
|
-
height: SAT_ICON_SIZE,
|
|
387
|
-
width: SAT_ICON_SIZE,
|
|
388
|
-
top: 1,
|
|
389
|
-
left: 1,
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
iconWrapper: {
|
|
393
|
-
display: "inline-block",
|
|
394
|
-
position: "relative",
|
|
395
|
-
},
|
|
396
|
-
|
|
397
|
-
crossOutLine: {
|
|
398
|
-
// Center the icon within the container.
|
|
399
|
-
position: "absolute",
|
|
400
|
-
top: `calc(50% - ${CROSS_OUT_LINE_SIZE / 2}px)`,
|
|
401
|
-
left: `calc(50% - ${CROSS_OUT_LINE_SIZE / 2}px)`,
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
libraryCircle: {
|
|
405
|
-
// Make the circle
|
|
406
|
-
width: LIBRARY_ICON_SIZE,
|
|
407
|
-
height: LIBRARY_ICON_SIZE,
|
|
408
|
-
boxSizing: "border-box",
|
|
409
|
-
borderRadius: LIBRARY_ICON_SIZE,
|
|
410
|
-
borderStyle: "solid",
|
|
411
|
-
borderWidth: 2,
|
|
412
|
-
|
|
413
|
-
// The default icons have letters in them. Style those letters.
|
|
414
|
-
fontFamily: styleConstants.baseFontFamily,
|
|
415
|
-
// NOTE(emily): We explicitly set the font weight instead of using the
|
|
416
|
-
// "bold font family" so that characters which fall back to the default
|
|
417
|
-
// font get bolded too.
|
|
418
|
-
fontWeight: "bold",
|
|
419
|
-
fontSize: 12,
|
|
420
|
-
|
|
421
|
-
// Center the contents of the icon.
|
|
422
|
-
display: "flex",
|
|
423
|
-
alignItems: "center",
|
|
424
|
-
justifyContent: "center",
|
|
425
|
-
// HACK(emily): I don't know why adding this line height makes the text
|
|
426
|
-
// appear centered better than any other value, but it does. In
|
|
427
|
-
// particular, at large zoom levels this line height does almost
|
|
428
|
-
// nothing, but at the default size this shifts the letter down one
|
|
429
|
-
// pixel so it is much better centered.
|
|
430
|
-
lineHeight: "1px",
|
|
431
|
-
},
|
|
432
|
-
|
|
433
|
-
libraryCircleCorrect: {
|
|
434
|
-
fontSize: LIBRARY_ICON_SIZE,
|
|
435
|
-
},
|
|
436
|
-
|
|
437
|
-
libraryCircleIncorrect: {
|
|
438
|
-
fontSize: LIBRARY_ICON_SIZE,
|
|
439
|
-
borderColor: styleConstants.gray68,
|
|
440
|
-
color: styleConstants.gray68,
|
|
441
|
-
},
|
|
442
|
-
|
|
443
|
-
libraryCircleIncorrectAnswered: {
|
|
444
|
-
backgroundColor: Color.red,
|
|
445
|
-
borderColor: Color.red,
|
|
446
|
-
color: Color.white,
|
|
447
|
-
},
|
|
448
|
-
|
|
449
|
-
letter: {
|
|
450
|
-
// These properties make sure that this element has the exact
|
|
451
|
-
// same size as `circle` so that we can center things
|
|
452
|
-
// inside of it.
|
|
453
|
-
border: "2px solid transparent",
|
|
454
|
-
width: SAT_ICON_SIZE,
|
|
455
|
-
height: SAT_ICON_SIZE,
|
|
456
|
-
position: "absolute",
|
|
457
|
-
top: 1,
|
|
458
|
-
|
|
459
|
-
// Center contained items.
|
|
460
|
-
display: "flex",
|
|
461
|
-
alignItems: "center",
|
|
462
|
-
justifyContent: "center",
|
|
463
|
-
|
|
464
|
-
fontFamily: styleConstants.boldFontFamily,
|
|
465
|
-
fontSize: 13,
|
|
466
|
-
},
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
export default ChoiceIcon;
|
|
@@ -1,134 +0,0 @@
|
|
|
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 Color 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 ChoiceIcon from "./choice-icon.jsx";
|
|
13
|
-
import {getChoiceLetter} from "./util.js";
|
|
14
|
-
|
|
15
|
-
export type CrossOutButtonProps = {
|
|
16
|
-
onClick: () => void,
|
|
17
|
-
onFocus: () => void,
|
|
18
|
-
onBlur: () => void,
|
|
19
|
-
crossedOut: boolean,
|
|
20
|
-
pos: number,
|
|
21
|
-
primaryProductColor?: string,
|
|
22
|
-
disabled?: boolean,
|
|
23
|
-
tabFocusable?: boolean,
|
|
24
|
-
usingKeyboardFocus: boolean,
|
|
25
|
-
// Day theme uses dark colors, for use on light backgrounds (tooltip).
|
|
26
|
-
// Night theme uses light colors, for use on dark backgrounds (slide panel).
|
|
27
|
-
//
|
|
28
|
-
// TODO(mdr): The day/night naming is used in Test Prep, but I've also seen
|
|
29
|
-
// a similar concept in another component (tooltip), as a boolean prop
|
|
30
|
-
// named `inverted`. If one of those emerges as the standard, we should
|
|
31
|
-
// switch to that!
|
|
32
|
-
theme: "day" | "night",
|
|
33
|
-
...
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
export default class CrossOutButton extends React.PureComponent<CrossOutButtonProps> {
|
|
37
|
-
static defaultProps: {|primaryProductColor: string|} = {
|
|
38
|
-
primaryProductColor: Color.blue,
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* A ref function for the tooltip button. Unlike most ref functions, where
|
|
43
|
-
* we save/delete the node on mount/unmount, we instead use this handler to
|
|
44
|
-
* auto-focus the button on mount, if appropriate.
|
|
45
|
-
*/
|
|
46
|
-
_handleTooltipButtonRef: (tooltipButton: ?HTMLButtonElement) => void = (
|
|
47
|
-
tooltipButton: ?HTMLButtonElement,
|
|
48
|
-
) => {
|
|
49
|
-
if (tooltipButton && this.props.usingKeyboardFocus) {
|
|
50
|
-
tooltipButton.focus();
|
|
51
|
-
}
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
_getAriaLabel(): string {
|
|
55
|
-
const {crossedOut, pos} = this.props;
|
|
56
|
-
const letter = getChoiceLetter(pos);
|
|
57
|
-
|
|
58
|
-
if (crossedOut) {
|
|
59
|
-
return i18n._("Bring back Choice %(letter)s", {letter});
|
|
60
|
-
}
|
|
61
|
-
return i18n._("Cross out Choice %(letter)s", {letter});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
render(): React.Element<"button"> {
|
|
65
|
-
const {crossedOut, pos, primaryProductColor, theme} = this.props;
|
|
66
|
-
|
|
67
|
-
const buttonAndTextColor =
|
|
68
|
-
theme === "day" ? primaryProductColor : Color.white;
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
aria-label={this._getAriaLabel()}
|
|
74
|
-
className={css([styles.tooltipButton])}
|
|
75
|
-
onClick={() => this.props.onClick()}
|
|
76
|
-
// Don't focus this element on mousedown. It's visually
|
|
77
|
-
// distracting, and confuses our "should we shift focus"
|
|
78
|
-
// logic for keyboard users.
|
|
79
|
-
onMouseDown={(e) => e.preventDefault()}
|
|
80
|
-
onFocus={() => this.props.onFocus()}
|
|
81
|
-
onBlur={() => this.props.onBlur()}
|
|
82
|
-
disabled={this.props.disabled}
|
|
83
|
-
tabIndex={this.props.tabFocusable ? 0 : -1}
|
|
84
|
-
ref={this._handleTooltipButtonRef}
|
|
85
|
-
>
|
|
86
|
-
<ChoiceIcon
|
|
87
|
-
pos={pos}
|
|
88
|
-
product="library"
|
|
89
|
-
// This icon is a preview of the _other_ state, so we use
|
|
90
|
-
// !crossedOut, the opposite of our current state.
|
|
91
|
-
crossedOut={!this.props.crossedOut}
|
|
92
|
-
// HACK(mdr): Here, we're using ChoiceIcon as a visual
|
|
93
|
-
// element: we want a non-filled bubble, with custom
|
|
94
|
-
// colored stroke/text rather than gray stroke/text.
|
|
95
|
-
// Unfortunately for us, ChoiceIcon has semantic props,
|
|
96
|
-
// so we have to hardcode the semantics that give us
|
|
97
|
-
// the visuals we want. This is a bit confusing and a
|
|
98
|
-
// bit fragile, but for now it seems better than adding
|
|
99
|
-
// complexity to the ChoiceIcon API!
|
|
100
|
-
primaryProductColor={buttonAndTextColor}
|
|
101
|
-
pressed={true}
|
|
102
|
-
checked={false}
|
|
103
|
-
focused={false}
|
|
104
|
-
correct={false}
|
|
105
|
-
showCorrectness={false}
|
|
106
|
-
reviewMode={false}
|
|
107
|
-
previouslyAnswered={false}
|
|
108
|
-
transparentBackground={true}
|
|
109
|
-
/>
|
|
110
|
-
<LabelLarge style={{color: buttonAndTextColor}}>
|
|
111
|
-
{crossedOut ? i18n._("Bring back") : i18n._("Cross out")}
|
|
112
|
-
</LabelLarge>
|
|
113
|
-
</button>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const styles = StyleSheet.create({
|
|
119
|
-
tooltipButton: {
|
|
120
|
-
// Reset <button> styles to be more like <div>.
|
|
121
|
-
background: "none",
|
|
122
|
-
border: "none",
|
|
123
|
-
font: "inherit",
|
|
124
|
-
padding: 0,
|
|
125
|
-
|
|
126
|
-
cursor: "pointer",
|
|
127
|
-
textDecoration: "none",
|
|
128
|
-
|
|
129
|
-
display: "flex",
|
|
130
|
-
flexDirection: "column",
|
|
131
|
-
alignItems: "center",
|
|
132
|
-
textAlign: "center",
|
|
133
|
-
},
|
|
134
|
-
});
|