@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.
- package/.eslintrc.js +6 -1
- package/CHANGELOG.md +45 -0
- package/dist/es/index.css +15 -8
- package/dist/es/index.js +1214 -1404
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +15 -8
- package/dist/index.js +1217 -1411
- package/dist/index.js.map +1 -1
- package/dist/strings.js +0 -4
- package/package.json +9 -8
- package/src/components/number-input.jsx +2 -2
- package/src/components/simple-keypad-input.jsx +18 -21
- package/src/index.js +1 -1
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
- package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
- package/src/perseus-api.jsx +10 -36
- package/src/perseus-types.js +18 -145
- package/src/styles/constants.js +0 -2
- package/src/styles/perseus-renderer.less +8 -2
- package/src/styles/variables.less +0 -2
- package/src/styles/widgets/passage.less +9 -5
- package/src/types.js +18 -35
- package/src/util/answer-types.js +2 -3
- package/src/util/math.js +3 -10
- package/src/util.js +0 -6
- package/src/version.js +1 -1
- package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
- package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
- package/src/widgets/__tests__/group_test.jsx +4 -4
- package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
- package/src/widgets/__tests__/passage_test.jsx +81 -0
- package/src/widgets/__tests__/radio/base-radio_test.jsx +291 -41
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +302 -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/__tests__/radio_test.jsx +39 -15
- package/src/widgets/definition.jsx +1 -1
- package/src/widgets/group.jsx +84 -102
- package/src/widgets/input-number.jsx +6 -1
- package/src/widgets/numeric-input.jsx +87 -80
- 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 +485 -556
- package/src/widgets/radio/choice-a11y-text.js +62 -0
- package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -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 +319 -347
- 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
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
3
3
|
import {StyleSheet, css} from "aphrodite";
|
|
4
4
|
import classNames from "classnames";
|
|
5
|
-
import PropTypes from "prop-types";
|
|
6
5
|
import * as React from "react";
|
|
6
|
+
import {useRef, useEffect} from "react";
|
|
7
7
|
import ReactDOM from "react-dom";
|
|
8
8
|
import _ from "underscore";
|
|
9
9
|
|
|
10
|
-
import {Errors as PerseusErrors, Log} from "../../logging/log.js";
|
|
11
10
|
import {ClassNames as ApiClassNames} from "../../perseus-api.jsx";
|
|
12
11
|
import * as styleConstants from "../../styles/constants.js";
|
|
13
12
|
import mediaQueries from "../../styles/media-queries.js";
|
|
@@ -18,259 +17,95 @@ import {scrollElementIntoView} from "../../util/scroll-utils.js";
|
|
|
18
17
|
import ChoiceNoneAbove from "./choice-none-above.jsx";
|
|
19
18
|
import Choice from "./choice.jsx";
|
|
20
19
|
|
|
20
|
+
import type {PerseusRadioWidgetOptions} from "../../perseus-types";
|
|
21
|
+
import type {APIOptions} from "../../types.js";
|
|
21
22
|
import type {StyleDeclaration} from "aphrodite";
|
|
22
23
|
|
|
23
24
|
const {captureScratchpadTouchStart} = Util;
|
|
24
25
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fontSize: 14,
|
|
89
|
-
lineHeight: 1.25,
|
|
90
|
-
fontStyle: "normal",
|
|
91
|
-
fontWeight: "bold",
|
|
92
|
-
marginBottom: 16,
|
|
93
|
-
},
|
|
94
|
-
|
|
95
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
96
|
-
instructionsMobile: {
|
|
97
|
-
fontSize: 18,
|
|
98
|
-
[mediaQueries.smOrSmaller]: {
|
|
99
|
-
fontSize: 16,
|
|
100
|
-
},
|
|
101
|
-
// TODO(emily): We want this to match choice text, which turns
|
|
102
|
-
// to 20px at min-width 1200px, but this media query is
|
|
103
|
-
// min-width 1280px because our media queries don't exactly
|
|
104
|
-
// match pure. Make those match up.
|
|
105
|
-
[mediaQueries.xl]: {
|
|
106
|
-
fontSize: 20,
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
111
|
-
radio: {
|
|
112
|
-
// Avoid centering
|
|
113
|
-
width: "100%",
|
|
114
|
-
},
|
|
115
|
-
|
|
116
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
117
|
-
responsiveRadioContainer: {
|
|
118
|
-
borderBottom: `1px solid ${radioBorderColor}`,
|
|
119
|
-
borderTop: `1px solid ${radioBorderColor}`,
|
|
120
|
-
width: "auto",
|
|
121
|
-
[mediaQueries.smOrSmaller]: {
|
|
122
|
-
marginLeft: styleConstants.negativePhoneMargin,
|
|
123
|
-
marginRight: styleConstants.negativePhoneMargin,
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
|
|
127
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
128
|
-
radioContainerFirstHighlighted: {
|
|
129
|
-
borderTop: `1px solid rgba(0, 0, 0, 0)`,
|
|
130
|
-
},
|
|
131
|
-
|
|
132
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
133
|
-
radioContainerLastHighlighted: {
|
|
134
|
-
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
138
|
-
satRadio: {
|
|
139
|
-
background: "none",
|
|
140
|
-
marginLeft: 0,
|
|
141
|
-
userSelect: "none",
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
145
|
-
satRadioOption: {
|
|
146
|
-
margin: 0,
|
|
147
|
-
padding: 0,
|
|
148
|
-
borderBottom: `1px solid #ccc`,
|
|
149
|
-
":first-child": {
|
|
150
|
-
borderTop: `1px solid #ccc`,
|
|
151
|
-
},
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
155
|
-
satRadioOptionCorrect: {
|
|
156
|
-
borderBottomColor: styleConstants.satCorrectBorderColor,
|
|
157
|
-
":first-child": {
|
|
158
|
-
borderTopColor: styleConstants.satCorrectBorderColor,
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
163
|
-
satRadioOptionIncorrect: {
|
|
164
|
-
borderBottomColor: styleConstants.satIncorrectBorderColor,
|
|
165
|
-
":first-child": {
|
|
166
|
-
borderTopColor: styleConstants.satIncorrectBorderColor,
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
|
|
170
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
171
|
-
satRadioOptionNextCorrect: {
|
|
172
|
-
borderBottomColor: styleConstants.satCorrectBorderColor,
|
|
173
|
-
},
|
|
174
|
-
|
|
175
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
176
|
-
satRadioOptionNextIncorrect: {
|
|
177
|
-
borderBottomColor: styleConstants.satIncorrectBorderColor,
|
|
178
|
-
},
|
|
179
|
-
|
|
180
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
181
|
-
satReviewRadioOption: {
|
|
182
|
-
pointerEvents: "none",
|
|
183
|
-
},
|
|
184
|
-
|
|
185
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
186
|
-
item: {
|
|
187
|
-
marginLeft: 20,
|
|
188
|
-
},
|
|
189
|
-
|
|
190
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
191
|
-
inlineItem: {
|
|
192
|
-
display: "inline-block",
|
|
193
|
-
paddingLeft: 20,
|
|
194
|
-
verticalAlign: "middle",
|
|
195
|
-
// See http://stackoverflow.com/q/8120466 for explanation of
|
|
196
|
-
// why vertical align property is needed
|
|
197
|
-
},
|
|
198
|
-
|
|
199
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
200
|
-
responsiveItem: {
|
|
201
|
-
marginLeft: 0,
|
|
202
|
-
padding: 0,
|
|
203
|
-
|
|
204
|
-
":not(:last-child)": {
|
|
205
|
-
borderBottom: `1px solid ${radioBorderColor}`,
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
|
|
209
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
210
|
-
selectedItem: {
|
|
211
|
-
background: "white",
|
|
212
|
-
},
|
|
213
|
-
|
|
214
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
215
|
-
aboveBackdrop: {
|
|
216
|
-
position: "relative",
|
|
217
|
-
// HACK(emily): We want selected choices to show up above our
|
|
218
|
-
// exercise backdrop, but below the exercise footer and
|
|
219
|
-
// "feedback popover" that shows up. This z-index is carefully
|
|
220
|
-
// coordinated between here and webapp. :(
|
|
221
|
-
zIndex: 1062,
|
|
222
|
-
},
|
|
223
|
-
|
|
224
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
225
|
-
aboveBackdropMobile: {
|
|
226
|
-
boxShadow:
|
|
227
|
-
"0 0 4px 0 rgba(0, 0, 0, 0.2)," +
|
|
228
|
-
"0 0 2px 0 rgba(0, 0, 0, 0.1)",
|
|
229
|
-
|
|
230
|
-
":not(:last-child)": {
|
|
231
|
-
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
232
|
-
},
|
|
233
|
-
},
|
|
234
|
-
|
|
235
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
236
|
-
nextHighlighted: {
|
|
237
|
-
":not(:last-child)": {
|
|
238
|
-
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
239
|
-
},
|
|
240
|
-
},
|
|
241
|
-
|
|
242
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
243
|
-
responsiveContainer: {
|
|
244
|
-
overflow: "auto",
|
|
245
|
-
marginLeft: styleConstants.negativePhoneMargin,
|
|
246
|
-
marginRight: styleConstants.negativePhoneMargin,
|
|
247
|
-
paddingLeft: styleConstants.phoneMargin,
|
|
248
|
-
// paddingRight is handled by responsiveFieldset
|
|
249
|
-
},
|
|
250
|
-
|
|
251
|
-
// eslint-disable-next-line react-native/no-unused-styles
|
|
252
|
-
responsiveFieldset: {
|
|
253
|
-
paddingRight: styleConstants.phoneMargin,
|
|
254
|
-
},
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
static defaultProps: $FlowFixMe = {
|
|
258
|
-
editMode: false,
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
state: $FlowFixMe = {
|
|
262
|
-
// TODO(mdr): This keeps the ID stable across re-renders on the
|
|
263
|
-
// same machine, but, at time of writing, the server's state
|
|
264
|
-
// isn't rehydrated to the client during SSR, so the server and
|
|
265
|
-
// client will generate different IDs and cause a mismatch
|
|
266
|
-
// during SSR :(
|
|
267
|
-
radioGroupName: _.uniqueId("perseus_radio_"),
|
|
268
|
-
};
|
|
269
|
-
|
|
270
|
-
componentDidUpdate(prevProps: $FlowFixMe) {
|
|
271
|
-
const {apiOptions, choices, isLastUsedWidget, reviewModeRubric} =
|
|
272
|
-
this.props;
|
|
26
|
+
// exported for tests
|
|
27
|
+
export type ChoiceType = {|
|
|
28
|
+
checked: boolean,
|
|
29
|
+
crossedOut: boolean,
|
|
30
|
+
content: React.Node,
|
|
31
|
+
rationale: React.Node,
|
|
32
|
+
hasRationale: boolean,
|
|
33
|
+
showRationale: boolean,
|
|
34
|
+
showCorrectness: boolean,
|
|
35
|
+
correct: boolean,
|
|
36
|
+
isNoneOfTheAbove: boolean,
|
|
37
|
+
highlighted: boolean,
|
|
38
|
+
previouslyAnswered: boolean,
|
|
39
|
+
revealNoneOfTheAbove: boolean,
|
|
40
|
+
disabled: boolean,
|
|
41
|
+
|};
|
|
42
|
+
|
|
43
|
+
export type FocusFunction = (choiceIndex: ?number) => boolean;
|
|
44
|
+
|
|
45
|
+
type Props = {|
|
|
46
|
+
apiOptions: APIOptions,
|
|
47
|
+
choices: $ReadOnlyArray<ChoiceType>,
|
|
48
|
+
deselectEnabled?: boolean,
|
|
49
|
+
editMode: boolean,
|
|
50
|
+
labelWrap: boolean,
|
|
51
|
+
countChoices: ?boolean,
|
|
52
|
+
numCorrect: number,
|
|
53
|
+
multipleSelect: boolean,
|
|
54
|
+
// the logic checks whether this exists,
|
|
55
|
+
// so it must be optional
|
|
56
|
+
reviewModeRubric?: PerseusRadioWidgetOptions,
|
|
57
|
+
|
|
58
|
+
// A callback indicating that this choice has changed. Its argument is
|
|
59
|
+
// an object with two keys: `checked` and `crossedOut`. Each contains
|
|
60
|
+
// an array of boolean values, specifying the new checked and
|
|
61
|
+
// crossed-out value of each choice.
|
|
62
|
+
onChange: (newValues: {
|
|
63
|
+
checked: $ReadOnlyArray<boolean>,
|
|
64
|
+
crossedOut: $ReadOnlyArray<boolean>,
|
|
65
|
+
}) => void,
|
|
66
|
+
registerFocusFunction?: (FocusFunction) => void,
|
|
67
|
+
|
|
68
|
+
// Whether this widget was the most recently used widget in this
|
|
69
|
+
// Renderer. Determines whether we'll auto-scroll the page upon
|
|
70
|
+
// entering review mode.
|
|
71
|
+
isLastUsedWidget?: boolean,
|
|
72
|
+
|};
|
|
73
|
+
|
|
74
|
+
function getInstructionsText(
|
|
75
|
+
multipleSelect: boolean,
|
|
76
|
+
countChoices: ?boolean,
|
|
77
|
+
numCorrect: number,
|
|
78
|
+
): string {
|
|
79
|
+
if (multipleSelect) {
|
|
80
|
+
if (countChoices) {
|
|
81
|
+
return i18n._("Choose %(numCorrect)s answers:", {
|
|
82
|
+
numCorrect: numCorrect,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return i18n._("Choose all answers that apply:");
|
|
86
|
+
}
|
|
87
|
+
return i18n._("Choose 1 answer:");
|
|
88
|
+
}
|
|
273
89
|
|
|
90
|
+
function BaseRadio(props: Props): React.Node {
|
|
91
|
+
const {
|
|
92
|
+
apiOptions,
|
|
93
|
+
reviewModeRubric,
|
|
94
|
+
choices,
|
|
95
|
+
editMode,
|
|
96
|
+
multipleSelect,
|
|
97
|
+
labelWrap,
|
|
98
|
+
countChoices,
|
|
99
|
+
numCorrect,
|
|
100
|
+
isLastUsedWidget,
|
|
101
|
+
registerFocusFunction,
|
|
102
|
+
} = props;
|
|
103
|
+
|
|
104
|
+
// useEffect doesn't have previous props
|
|
105
|
+
const prevReviewModeRubric = useRef();
|
|
106
|
+
const choiceRefs = useRef([]);
|
|
107
|
+
|
|
108
|
+
useEffect(() => {
|
|
274
109
|
// Switching into review mode can sometimes cause the selected answer
|
|
275
110
|
// to scroll out of view - for example, when we reveal all those tall
|
|
276
111
|
// rationales. This can be disorienting for the user.
|
|
@@ -288,18 +123,24 @@ class BaseRadio extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
|
288
123
|
apiOptions.canScrollPage &&
|
|
289
124
|
isLastUsedWidget &&
|
|
290
125
|
reviewModeRubric &&
|
|
291
|
-
!
|
|
126
|
+
!prevReviewModeRubric.current
|
|
292
127
|
) {
|
|
293
128
|
const checkedIndex = choices.findIndex((c) => c.checked);
|
|
294
129
|
if (checkedIndex >= 0) {
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
130
|
+
const ref = choiceRefs.current[checkedIndex];
|
|
131
|
+
// note(matthew): we know this is only getting passed
|
|
132
|
+
// to a WB Clickable button, so we force it to be of
|
|
133
|
+
// type HTMLButtonElement
|
|
134
|
+
const anyNode = (ReactDOM.findDOMNode(ref.current): any);
|
|
135
|
+
const buttonNode = (anyNode: ?HTMLButtonElement);
|
|
136
|
+
if (buttonNode) {
|
|
137
|
+
scrollElementIntoView(buttonNode);
|
|
138
|
+
}
|
|
300
139
|
}
|
|
301
140
|
}
|
|
302
|
-
|
|
141
|
+
|
|
142
|
+
prevReviewModeRubric.current = reviewModeRubric;
|
|
143
|
+
}, [apiOptions, choices, isLastUsedWidget, reviewModeRubric]);
|
|
303
144
|
|
|
304
145
|
// When a particular choice's `onChange` handler is called, indicating a
|
|
305
146
|
// change in a single choice's values, we need to call our `onChange`
|
|
@@ -310,333 +151,421 @@ class BaseRadio extends React.Component<$FlowFixMe, $FlowFixMe> {
|
|
|
310
151
|
// will usually cause the old answer to become unchecked.)
|
|
311
152
|
//
|
|
312
153
|
// So, given the new values for a particular choice, compute the new values
|
|
313
|
-
// for all choices, and pass them to `
|
|
154
|
+
// for all choices, and pass them to `onChange`.
|
|
314
155
|
//
|
|
315
156
|
// `newValues` is an object with two keys: `checked` and `crossedOut`. Each
|
|
316
157
|
// contains a boolean value specifying the new checked and crossed-out
|
|
317
158
|
// value of this choice.
|
|
318
|
-
updateChoice
|
|
159
|
+
function updateChoice(
|
|
160
|
+
choiceIndex: number,
|
|
161
|
+
newValues: $ReadOnly<{checked: boolean, crossedOut: boolean}>,
|
|
162
|
+
): void {
|
|
163
|
+
const {multipleSelect, choices, onChange} = props;
|
|
164
|
+
|
|
319
165
|
// Get the baseline `checked` values. If we're checking a new answer
|
|
320
166
|
// and multiple-select is not on, we should clear all choices to be
|
|
321
167
|
// unchecked. Otherwise, we should copy the old checked values.
|
|
322
168
|
let newCheckedList;
|
|
323
|
-
if (newValues.checked && !
|
|
324
|
-
newCheckedList =
|
|
169
|
+
if (newValues.checked && !multipleSelect) {
|
|
170
|
+
newCheckedList = choices.map((_) => false);
|
|
325
171
|
} else {
|
|
326
|
-
newCheckedList =
|
|
172
|
+
newCheckedList = choices.map((c) => c.checked);
|
|
327
173
|
}
|
|
328
174
|
|
|
329
175
|
// Get the baseline `crossedOut` values.
|
|
330
|
-
const newCrossedOutList =
|
|
176
|
+
const newCrossedOutList = choices.map((c) => c.crossedOut);
|
|
331
177
|
|
|
332
178
|
// Update this choice's `checked` and `crossedOut` values.
|
|
333
179
|
newCheckedList[choiceIndex] = newValues.checked;
|
|
334
180
|
newCrossedOutList[choiceIndex] = newValues.crossedOut;
|
|
335
181
|
|
|
336
|
-
|
|
182
|
+
onChange({
|
|
337
183
|
checked: newCheckedList,
|
|
338
184
|
crossedOut: newCrossedOutList,
|
|
339
185
|
});
|
|
340
|
-
}
|
|
186
|
+
}
|
|
341
187
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
return i18n._("Choose all answers that apply:");
|
|
357
|
-
}
|
|
358
|
-
return i18n._("Choose 1 answer:");
|
|
359
|
-
};
|
|
360
|
-
|
|
361
|
-
deselectEnabled: () => boolean = () => {
|
|
362
|
-
// We want to force enable deselect on mobile.
|
|
363
|
-
return this.props.apiOptions.isMobile || this.props.deselectEnabled;
|
|
364
|
-
};
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Find the choice at the given index (wrapped, if necessary), focus it,
|
|
368
|
-
* uncheck the old choice, and check the new choice (unless it's crossed
|
|
369
|
-
* out, in which case we only uncheck the old choice).
|
|
370
|
-
*
|
|
371
|
-
* This is very similar to standard up/down arrow behavior, except that we
|
|
372
|
-
* don't select crossed-out choices.
|
|
373
|
-
*
|
|
374
|
-
* Handles `goToPrevChoice` and `goToNextChoice` calls from our children.
|
|
375
|
-
*/
|
|
376
|
-
goToChoice: (number) => void = (newChoiceIndex) => {
|
|
377
|
-
const numChoices = this.props.choices.length;
|
|
378
|
-
|
|
379
|
-
// Wrap the newChoiceIndex around the start/end, if necessary.
|
|
380
|
-
if (newChoiceIndex < 0) {
|
|
381
|
-
newChoiceIndex += numChoices;
|
|
382
|
-
}
|
|
383
|
-
if (newChoiceIndex >= numChoices) {
|
|
384
|
-
newChoiceIndex -= numChoices;
|
|
188
|
+
// register a callback with the parent that allows
|
|
189
|
+
// the parent to focus an individual choice
|
|
190
|
+
registerFocusFunction?.((choiceIndex) => {
|
|
191
|
+
const ref = choiceRefs.current[choiceIndex || 0];
|
|
192
|
+
// note(matthew): we know this is only getting passed
|
|
193
|
+
// to a WB Clickable button, so we force it to be of
|
|
194
|
+
// type HTMLButtonElement
|
|
195
|
+
const anyNode = (ReactDOM.findDOMNode(ref.current): any);
|
|
196
|
+
const buttonNode = (anyNode: ?HTMLButtonElement);
|
|
197
|
+
|
|
198
|
+
if (buttonNode) {
|
|
199
|
+
buttonNode.focus();
|
|
200
|
+
} else {
|
|
201
|
+
return false;
|
|
385
202
|
}
|
|
203
|
+
return true;
|
|
204
|
+
});
|
|
386
205
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
206
|
+
// some commonly used shorthands
|
|
207
|
+
const reviewMode = !!reviewModeRubric;
|
|
208
|
+
const sat = apiOptions.satStyling;
|
|
209
|
+
const isMobile = apiOptions.isMobile;
|
|
210
|
+
|
|
211
|
+
const firstChoiceHighlighted = choices[0].highlighted;
|
|
212
|
+
const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
|
|
213
|
+
|
|
214
|
+
const className: $ReadOnlyArray<string> = classNames(
|
|
215
|
+
"perseus-widget-radio",
|
|
216
|
+
!editMode && "perseus-rendered-radio",
|
|
217
|
+
css(
|
|
218
|
+
styles.radio,
|
|
219
|
+
// SAT doesn't use the "responsive styling" as it conflicts
|
|
220
|
+
// with their custom theming.
|
|
221
|
+
!sat && styles.responsiveRadioContainer,
|
|
222
|
+
!sat &&
|
|
223
|
+
firstChoiceHighlighted &&
|
|
224
|
+
isMobile &&
|
|
225
|
+
styles.radioContainerFirstHighlighted,
|
|
226
|
+
!sat &&
|
|
227
|
+
lastChoiceHighlighted &&
|
|
228
|
+
isMobile &&
|
|
229
|
+
styles.radioContainerLastHighlighted,
|
|
230
|
+
sat && styles.satRadio,
|
|
231
|
+
),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const instructionsClassName: $ReadOnlyArray<string> = classNames(
|
|
235
|
+
"instructions",
|
|
236
|
+
css(styles.instructions, isMobile && styles.instructionsMobile),
|
|
237
|
+
);
|
|
238
|
+
const instructions = getInstructionsText(
|
|
239
|
+
multipleSelect,
|
|
240
|
+
countChoices,
|
|
241
|
+
numCorrect,
|
|
242
|
+
);
|
|
243
|
+
const shouldShowInstructions = !sat;
|
|
244
|
+
|
|
245
|
+
const responsiveClassName = css(styles.responsiveFieldset);
|
|
246
|
+
const fieldset = (
|
|
247
|
+
<fieldset
|
|
248
|
+
className={`perseus-widget-radio-fieldset ${responsiveClassName}`}
|
|
249
|
+
>
|
|
250
|
+
<legend className="perseus-sr-only">{instructions}</legend>
|
|
251
|
+
{shouldShowInstructions && (
|
|
252
|
+
<div className={instructionsClassName} aria-hidden="true">
|
|
253
|
+
{instructions}
|
|
254
|
+
</div>
|
|
255
|
+
)}
|
|
256
|
+
<ul className={className} style={{listStyle: "none"}}>
|
|
257
|
+
{choices.map(function (choice, i) {
|
|
258
|
+
let Element = Choice;
|
|
259
|
+
const ref = React.createRef();
|
|
260
|
+
choiceRefs.current[i] = ref;
|
|
261
|
+
const elementProps = {
|
|
262
|
+
apiOptions: apiOptions,
|
|
263
|
+
multipleSelect: multipleSelect,
|
|
264
|
+
checked: choice.checked,
|
|
265
|
+
crossedOut: choice.crossedOut,
|
|
266
|
+
previouslyAnswered: choice.previouslyAnswered,
|
|
267
|
+
reviewMode,
|
|
268
|
+
correct: choice.correct,
|
|
269
|
+
rationale: choice.rationale,
|
|
270
|
+
content: choice.content,
|
|
271
|
+
disabled: apiOptions.readOnly || choice.disabled,
|
|
272
|
+
showCorrectness: reviewMode || !!choice.showCorrectness,
|
|
273
|
+
showRationale:
|
|
274
|
+
choice.hasRationale &&
|
|
275
|
+
(reviewMode || choice.showRationale),
|
|
276
|
+
pos: i,
|
|
277
|
+
onChange: (newValues) => {
|
|
278
|
+
updateChoice(i, newValues);
|
|
279
|
+
},
|
|
280
|
+
ref,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
if (choice.isNoneOfTheAbove) {
|
|
284
|
+
Element = ChoiceNoneAbove;
|
|
285
|
+
_.extend(elementProps, {
|
|
286
|
+
showContent: choice.revealNoneOfTheAbove,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const nextChoice = choices[i + 1];
|
|
291
|
+
const nextChoiceHighlighted =
|
|
292
|
+
!!nextChoice && nextChoice.highlighted;
|
|
293
|
+
|
|
294
|
+
const aphroditeClassName = (checked) => {
|
|
295
|
+
// Whether or not to show correctness borders
|
|
296
|
+
// for this choice and the next choice.
|
|
297
|
+
const satShowCorrectness = sat && reviewMode && checked;
|
|
298
|
+
const satShowCorrectnessNext =
|
|
299
|
+
sat &&
|
|
300
|
+
reviewMode &&
|
|
301
|
+
nextChoice &&
|
|
302
|
+
nextChoice.checked;
|
|
303
|
+
|
|
304
|
+
return css(
|
|
305
|
+
sharedStyles.aboveScratchpad,
|
|
306
|
+
styles.item,
|
|
307
|
+
!sat && styles.responsiveItem,
|
|
308
|
+
!sat && checked && styles.selectedItem,
|
|
309
|
+
!sat &&
|
|
310
|
+
checked &&
|
|
311
|
+
choice.highlighted &&
|
|
312
|
+
styles.aboveBackdrop,
|
|
313
|
+
!sat &&
|
|
314
|
+
checked &&
|
|
315
|
+
choice.highlighted &&
|
|
316
|
+
apiOptions.isMobile &&
|
|
317
|
+
styles.aboveBackdropMobile,
|
|
318
|
+
!sat &&
|
|
319
|
+
nextChoiceHighlighted &&
|
|
320
|
+
apiOptions.isMobile &&
|
|
321
|
+
styles.nextHighlighted,
|
|
322
|
+
sat && styles.satRadioOption,
|
|
323
|
+
satShowCorrectness &&
|
|
324
|
+
!choice.correct &&
|
|
325
|
+
styles.satRadioOptionIncorrect,
|
|
326
|
+
satShowCorrectness &&
|
|
327
|
+
choice.correct &&
|
|
328
|
+
styles.satRadioOptionCorrect,
|
|
329
|
+
satShowCorrectnessNext &&
|
|
330
|
+
!nextChoice.correct &&
|
|
331
|
+
styles.satRadioOptionNextIncorrect,
|
|
332
|
+
satShowCorrectnessNext &&
|
|
333
|
+
nextChoice.correct &&
|
|
334
|
+
styles.satRadioOptionNextCorrect,
|
|
335
|
+
sat &&
|
|
336
|
+
reviewModeRubric &&
|
|
337
|
+
styles.satReviewRadioOption,
|
|
338
|
+
);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// HACK(abdulrahman): Preloads the selection-state
|
|
342
|
+
// css because of a bug that causes iOS to lag
|
|
343
|
+
// when selecting the button for the first time.
|
|
344
|
+
aphroditeClassName(true);
|
|
345
|
+
|
|
346
|
+
let correctnessClass;
|
|
347
|
+
// reviewMode is only true if there's a rubric
|
|
348
|
+
// but Flow doesn't understand that
|
|
349
|
+
if (reviewMode && reviewModeRubric) {
|
|
350
|
+
correctnessClass = reviewModeRubric.choices[i].correct
|
|
351
|
+
? ApiClassNames.CORRECT
|
|
352
|
+
: ApiClassNames.INCORRECT;
|
|
353
|
+
}
|
|
354
|
+
const className: $ReadOnlyArray<string> = classNames(
|
|
355
|
+
aphroditeClassName(choice.checked),
|
|
356
|
+
// TODO(aria): Make test case for these API
|
|
357
|
+
// classNames
|
|
358
|
+
ApiClassNames.RADIO.OPTION,
|
|
359
|
+
choice.checked && ApiClassNames.RADIO.SELECTED,
|
|
360
|
+
correctnessClass,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// In edit mode, the Choice renders a Div in order to
|
|
364
|
+
// allow for the contentEditable area to be selected
|
|
365
|
+
// (label forces any clicks inside to select the input
|
|
366
|
+
// element) We have to add some extra behavior to make
|
|
367
|
+
// sure that we can still check the choice.
|
|
368
|
+
let listElem = null;
|
|
369
|
+
let clickHandler = null;
|
|
370
|
+
if (editMode) {
|
|
371
|
+
clickHandler = (e) => {
|
|
372
|
+
// Traverse the parent nodes of the clicked
|
|
373
|
+
// element.
|
|
374
|
+
let elem = e.target;
|
|
375
|
+
while (elem && elem !== listElem) {
|
|
376
|
+
// If the clicked element is inside of the
|
|
377
|
+
// radio icon, then we want to trigger the
|
|
378
|
+
// check by flipping the choice of the icon.
|
|
379
|
+
if (elem.getAttribute("data-is-radio-icon")) {
|
|
380
|
+
this.updateChoice(i, {
|
|
381
|
+
checked: !choice.checked,
|
|
382
|
+
crossedOut: choice.crossedOut,
|
|
383
|
+
});
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
elem = elem.parentNode;
|
|
387
|
+
}
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// TODO(mattdr): Index isn't a *good* choice of key
|
|
392
|
+
// here; is there a better one? Can we use choice
|
|
393
|
+
// content somehow? Would changing our choice of key
|
|
394
|
+
// somehow break something happening inside a choice's
|
|
395
|
+
// child Renderers, by changing when we mount/unmount?
|
|
396
|
+
return (
|
|
397
|
+
<li
|
|
398
|
+
key={i}
|
|
399
|
+
ref={(e) => (listElem = e)}
|
|
400
|
+
className={className}
|
|
401
|
+
onClick={clickHandler}
|
|
402
|
+
onTouchStart={
|
|
403
|
+
!labelWrap ? null : captureScratchpadTouchStart
|
|
404
|
+
}
|
|
405
|
+
>
|
|
406
|
+
<Element {...elementProps} />
|
|
407
|
+
</li>
|
|
408
|
+
);
|
|
409
|
+
}, this)}
|
|
410
|
+
</ul>
|
|
411
|
+
</fieldset>
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Allow for horizontal scrolling if content is too wide, which may be
|
|
415
|
+
// an issue especially on phones.
|
|
416
|
+
// This is disabled in SAT, since it conflicts with their theming.
|
|
417
|
+
return (
|
|
418
|
+
<div className={css(!sat && styles.responsiveContainer)}>
|
|
419
|
+
{fieldset}
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
400
423
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
424
|
+
BaseRadio.defaultProps = {
|
|
425
|
+
editMode: false,
|
|
426
|
+
multipleSelect: false,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const styles: StyleDeclaration = StyleSheet.create({
|
|
430
|
+
instructions: {
|
|
431
|
+
display: "block",
|
|
432
|
+
color: styleConstants.gray17,
|
|
433
|
+
fontSize: 14,
|
|
434
|
+
lineHeight: 1.25,
|
|
435
|
+
fontStyle: "normal",
|
|
436
|
+
fontWeight: "bold",
|
|
437
|
+
marginBottom: 16,
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
instructionsMobile: {
|
|
441
|
+
fontSize: 18,
|
|
442
|
+
[mediaQueries.smOrSmaller]: {
|
|
443
|
+
fontSize: 16,
|
|
444
|
+
},
|
|
445
|
+
// TODO(emily): We want this to match choice text, which turns
|
|
446
|
+
// to 20px at min-width 1200px, but this media query is
|
|
447
|
+
// min-width 1280px because our media queries don't exactly
|
|
448
|
+
// match pure. Make those match up.
|
|
449
|
+
[mediaQueries.xl]: {
|
|
450
|
+
fontSize: 20,
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
radio: {
|
|
455
|
+
// Avoid centering
|
|
456
|
+
width: "100%",
|
|
457
|
+
},
|
|
458
|
+
|
|
459
|
+
responsiveRadioContainer: {
|
|
460
|
+
borderBottom: `1px solid ${styleConstants.radioBorderColor}`,
|
|
461
|
+
borderTop: `1px solid ${styleConstants.radioBorderColor}`,
|
|
462
|
+
width: "auto",
|
|
463
|
+
[mediaQueries.smOrSmaller]: {
|
|
464
|
+
marginLeft: styleConstants.negativePhoneMargin,
|
|
465
|
+
marginRight: styleConstants.negativePhoneMargin,
|
|
466
|
+
},
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
radioContainerFirstHighlighted: {
|
|
470
|
+
borderTop: `1px solid rgba(0, 0, 0, 0)`,
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
radioContainerLastHighlighted: {
|
|
474
|
+
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
475
|
+
},
|
|
476
|
+
|
|
477
|
+
satRadio: {
|
|
478
|
+
background: "none",
|
|
479
|
+
marginLeft: 0,
|
|
480
|
+
userSelect: "none",
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
satRadioOption: {
|
|
484
|
+
margin: 0,
|
|
485
|
+
padding: 0,
|
|
486
|
+
borderBottom: `1px solid #ccc`,
|
|
487
|
+
":first-child": {
|
|
488
|
+
borderTop: `1px solid #ccc`,
|
|
489
|
+
},
|
|
490
|
+
},
|
|
405
491
|
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
492
|
+
satRadioOptionCorrect: {
|
|
493
|
+
borderBottomColor: styleConstants.satCorrectBorderColor,
|
|
494
|
+
":first-child": {
|
|
495
|
+
borderTopColor: styleConstants.satCorrectBorderColor,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
410
498
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
render(): React.Node {
|
|
418
|
-
const inputType = this.props.multipleSelect ? "checkbox" : "radio";
|
|
419
|
-
const rubric = this.props.reviewModeRubric;
|
|
420
|
-
const reviewMode = !!rubric;
|
|
421
|
-
|
|
422
|
-
const styles = BaseRadio.styles;
|
|
423
|
-
const sat = this.props.apiOptions.satStyling;
|
|
424
|
-
|
|
425
|
-
const isMobile = this.props.apiOptions.isMobile;
|
|
426
|
-
|
|
427
|
-
const choices = this.props.choices;
|
|
428
|
-
const firstChoiceHighlighted = choices[0].highlighted;
|
|
429
|
-
const lastChoiceHighlighted = choices[choices.length - 1].highlighted;
|
|
430
|
-
|
|
431
|
-
const className = classNames(
|
|
432
|
-
"perseus-widget-radio",
|
|
433
|
-
!this.props.editMode && "perseus-rendered-radio",
|
|
434
|
-
css(
|
|
435
|
-
styles.radio,
|
|
436
|
-
// SAT doesn't use the "responsive styling" as it conflicts
|
|
437
|
-
// with their custom theming.
|
|
438
|
-
!sat && styles.responsiveRadioContainer,
|
|
439
|
-
!sat &&
|
|
440
|
-
firstChoiceHighlighted &&
|
|
441
|
-
isMobile &&
|
|
442
|
-
styles.radioContainerFirstHighlighted,
|
|
443
|
-
!sat &&
|
|
444
|
-
lastChoiceHighlighted &&
|
|
445
|
-
isMobile &&
|
|
446
|
-
styles.radioContainerLastHighlighted,
|
|
447
|
-
sat && styles.satRadio,
|
|
448
|
-
),
|
|
449
|
-
);
|
|
450
|
-
|
|
451
|
-
const instructionsClassName = classNames(
|
|
452
|
-
"instructions",
|
|
453
|
-
css(styles.instructions, isMobile && styles.instructionsMobile),
|
|
454
|
-
);
|
|
455
|
-
const instructions = this.getInstructionsText();
|
|
456
|
-
const shouldShowInstructions = !sat;
|
|
457
|
-
|
|
458
|
-
const responsiveClassName = css(styles.responsiveFieldset);
|
|
459
|
-
const fieldset = (
|
|
460
|
-
<fieldset
|
|
461
|
-
className={`perseus-widget-radio-fieldset ${responsiveClassName}`}
|
|
462
|
-
>
|
|
463
|
-
<legend className="perseus-sr-only">{instructions}</legend>
|
|
464
|
-
{shouldShowInstructions && (
|
|
465
|
-
<div className={instructionsClassName}>{instructions}</div>
|
|
466
|
-
)}
|
|
467
|
-
<ul className={className} style={{listStyle: "none"}}>
|
|
468
|
-
{this.props.choices.map(function (choice, i) {
|
|
469
|
-
let Element = Choice;
|
|
470
|
-
const elementProps = {
|
|
471
|
-
ref: `radio${i}`,
|
|
472
|
-
apiOptions: this.props.apiOptions,
|
|
473
|
-
checked: choice.checked,
|
|
474
|
-
crossedOut: choice.crossedOut,
|
|
475
|
-
previouslyAnswered: choice.previouslyAnswered,
|
|
476
|
-
reviewMode,
|
|
477
|
-
correct: choice.correct,
|
|
478
|
-
rationale: choice.rationale,
|
|
479
|
-
content: choice.content,
|
|
480
|
-
disabled:
|
|
481
|
-
this.props.apiOptions.readOnly ||
|
|
482
|
-
choice.disabled,
|
|
483
|
-
editMode: this.props.editMode,
|
|
484
|
-
groupName: this.state.radioGroupName,
|
|
485
|
-
isLastChoice: i === this.props.choices.length - 1,
|
|
486
|
-
showCorrectness:
|
|
487
|
-
reviewMode || !!choice.showCorrectness,
|
|
488
|
-
showRationale:
|
|
489
|
-
choice.hasRationale &&
|
|
490
|
-
(reviewMode || choice.showRationale),
|
|
491
|
-
type: inputType,
|
|
492
|
-
pos: i,
|
|
493
|
-
deselectEnabled: this.deselectEnabled(),
|
|
494
|
-
onChange: (newValues) => {
|
|
495
|
-
this.updateChoice(i, newValues);
|
|
496
|
-
},
|
|
497
|
-
goToPrevChoice: () => this.goToChoice(i - 1),
|
|
498
|
-
goToNextChoice: () => this.goToChoice(i + 1),
|
|
499
|
-
};
|
|
499
|
+
satRadioOptionIncorrect: {
|
|
500
|
+
borderBottomColor: styleConstants.satIncorrectBorderColor,
|
|
501
|
+
":first-child": {
|
|
502
|
+
borderTopColor: styleConstants.satIncorrectBorderColor,
|
|
503
|
+
},
|
|
504
|
+
},
|
|
500
505
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
showContent: choice.revealNoneOfTheAbove,
|
|
505
|
-
});
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const nextChoice = this.props.choices[i + 1];
|
|
509
|
-
const nextChoiceHighlighted =
|
|
510
|
-
!!nextChoice && nextChoice.highlighted;
|
|
511
|
-
|
|
512
|
-
const aphroditeClassName = (checked) => {
|
|
513
|
-
// Whether or not to show correctness borders
|
|
514
|
-
// for this choice and the next choice.
|
|
515
|
-
const satShowCorrectness =
|
|
516
|
-
sat && reviewMode && checked;
|
|
517
|
-
const satShowCorrectnessNext =
|
|
518
|
-
sat &&
|
|
519
|
-
reviewMode &&
|
|
520
|
-
nextChoice &&
|
|
521
|
-
nextChoice.checked;
|
|
522
|
-
|
|
523
|
-
return css(
|
|
524
|
-
sharedStyles.aboveScratchpad,
|
|
525
|
-
styles.item,
|
|
526
|
-
!sat && styles.responsiveItem,
|
|
527
|
-
!sat && checked && styles.selectedItem,
|
|
528
|
-
!sat &&
|
|
529
|
-
checked &&
|
|
530
|
-
choice.highlighted &&
|
|
531
|
-
styles.aboveBackdrop,
|
|
532
|
-
!sat &&
|
|
533
|
-
checked &&
|
|
534
|
-
choice.highlighted &&
|
|
535
|
-
this.props.apiOptions.isMobile &&
|
|
536
|
-
styles.aboveBackdropMobile,
|
|
537
|
-
!sat &&
|
|
538
|
-
nextChoiceHighlighted &&
|
|
539
|
-
this.props.apiOptions.isMobile &&
|
|
540
|
-
styles.nextHighlighted,
|
|
541
|
-
sat && styles.satRadioOption,
|
|
542
|
-
satShowCorrectness &&
|
|
543
|
-
!choice.correct &&
|
|
544
|
-
styles.satRadioOptionIncorrect,
|
|
545
|
-
satShowCorrectness &&
|
|
546
|
-
choice.correct &&
|
|
547
|
-
styles.satRadioOptionCorrect,
|
|
548
|
-
satShowCorrectnessNext &&
|
|
549
|
-
!nextChoice.correct &&
|
|
550
|
-
styles.satRadioOptionNextIncorrect,
|
|
551
|
-
satShowCorrectnessNext &&
|
|
552
|
-
nextChoice.correct &&
|
|
553
|
-
styles.satRadioOptionNextCorrect,
|
|
554
|
-
sat && rubric && styles.satReviewRadioOption,
|
|
555
|
-
);
|
|
556
|
-
};
|
|
506
|
+
satRadioOptionNextCorrect: {
|
|
507
|
+
borderBottomColor: styleConstants.satCorrectBorderColor,
|
|
508
|
+
},
|
|
557
509
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
aphroditeClassName(true);
|
|
562
|
-
|
|
563
|
-
const className = classNames(
|
|
564
|
-
aphroditeClassName(choice.checked),
|
|
565
|
-
// TODO(aria): Make test case for these API
|
|
566
|
-
// classNames
|
|
567
|
-
ApiClassNames.RADIO.OPTION,
|
|
568
|
-
choice.checked && ApiClassNames.RADIO.SELECTED,
|
|
569
|
-
reviewMode &&
|
|
570
|
-
rubric.choices[i].correct &&
|
|
571
|
-
ApiClassNames.CORRECT,
|
|
572
|
-
reviewMode &&
|
|
573
|
-
!rubric.choices[i].correct &&
|
|
574
|
-
ApiClassNames.INCORRECT,
|
|
575
|
-
);
|
|
510
|
+
satRadioOptionNextIncorrect: {
|
|
511
|
+
borderBottomColor: styleConstants.satIncorrectBorderColor,
|
|
512
|
+
},
|
|
576
513
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
// This is disabled in SAT, since it conflicts with their theming.
|
|
634
|
-
return (
|
|
635
|
-
<div className={css(!sat && styles.responsiveContainer)}>
|
|
636
|
-
{fieldset}
|
|
637
|
-
</div>
|
|
638
|
-
);
|
|
639
|
-
}
|
|
640
|
-
}
|
|
514
|
+
satReviewRadioOption: {
|
|
515
|
+
pointerEvents: "none",
|
|
516
|
+
},
|
|
517
|
+
|
|
518
|
+
item: {
|
|
519
|
+
marginLeft: 20,
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
responsiveItem: {
|
|
523
|
+
marginLeft: 0,
|
|
524
|
+
padding: 0,
|
|
525
|
+
|
|
526
|
+
":not(:last-child)": {
|
|
527
|
+
borderBottom: `1px solid ${styleConstants.radioBorderColor}`,
|
|
528
|
+
},
|
|
529
|
+
},
|
|
530
|
+
selectedItem: {
|
|
531
|
+
background: "white",
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
aboveBackdrop: {
|
|
535
|
+
position: "relative",
|
|
536
|
+
// HACK(emily): We want selected choices to show up above our
|
|
537
|
+
// exercise backdrop, but below the exercise footer and
|
|
538
|
+
// "feedback popover" that shows up. This z-index is carefully
|
|
539
|
+
// coordinated between here and webapp. :(
|
|
540
|
+
zIndex: 1062,
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
aboveBackdropMobile: {
|
|
544
|
+
boxShadow:
|
|
545
|
+
"0 0 4px 0 rgba(0, 0, 0, 0.2)," + "0 0 2px 0 rgba(0, 0, 0, 0.1)",
|
|
546
|
+
|
|
547
|
+
":not(:last-child)": {
|
|
548
|
+
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
|
|
552
|
+
nextHighlighted: {
|
|
553
|
+
":not(:last-child)": {
|
|
554
|
+
borderBottom: `1px solid rgba(0, 0, 0, 0)`,
|
|
555
|
+
},
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
responsiveContainer: {
|
|
559
|
+
overflow: "auto",
|
|
560
|
+
marginLeft: styleConstants.negativePhoneMargin,
|
|
561
|
+
marginRight: styleConstants.negativePhoneMargin,
|
|
562
|
+
paddingLeft: styleConstants.phoneMargin,
|
|
563
|
+
// paddingRight is handled by responsiveFieldset
|
|
564
|
+
},
|
|
565
|
+
|
|
566
|
+
responsiveFieldset: {
|
|
567
|
+
paddingRight: styleConstants.phoneMargin,
|
|
568
|
+
},
|
|
569
|
+
});
|
|
641
570
|
|
|
642
571
|
export default BaseRadio;
|