@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
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
4
4
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
5
5
|
import * as React from "react";
|
|
6
|
-
import _ from "underscore";
|
|
7
6
|
|
|
8
7
|
import Renderer from "../../renderer.jsx";
|
|
9
8
|
import Util from "../../util.js";
|
|
@@ -15,7 +14,13 @@ import type {
|
|
|
15
14
|
PerseusRadioChoice,
|
|
16
15
|
PerseusRadioWidgetOptions,
|
|
17
16
|
} from "../../perseus-types.js";
|
|
18
|
-
import type {
|
|
17
|
+
import type {
|
|
18
|
+
PerseusScore,
|
|
19
|
+
WidgetProps,
|
|
20
|
+
ChoiceState,
|
|
21
|
+
RadioChoiceWithMetadata,
|
|
22
|
+
} from "../../types.js";
|
|
23
|
+
import type {FocusFunction, ChoiceType} from "./base-radio.jsx";
|
|
19
24
|
|
|
20
25
|
// RenderProps is the return type for radio.jsx#transform
|
|
21
26
|
export type RenderProps = {|
|
|
@@ -24,9 +29,12 @@ export type RenderProps = {|
|
|
|
24
29
|
multipleSelect?: boolean,
|
|
25
30
|
countChoices?: boolean,
|
|
26
31
|
deselectEnabled?: boolean,
|
|
27
|
-
choices: $ReadOnlyArray<
|
|
32
|
+
choices: $ReadOnlyArray<RadioChoiceWithMetadata>,
|
|
28
33
|
selectedChoices: $ReadOnlyArray<PerseusRadioChoice["correct"]>,
|
|
29
|
-
choiceStates?: $ReadOnlyArray
|
|
34
|
+
choiceStates?: $ReadOnlyArray<ChoiceState>,
|
|
35
|
+
// Depreciated; support for legacy way of handling changes
|
|
36
|
+
// Adds proptype for prop that is used but was lacking type
|
|
37
|
+
values?: $ReadOnlyArray<boolean>,
|
|
30
38
|
|};
|
|
31
39
|
|
|
32
40
|
type UserInput = {|
|
|
@@ -39,25 +47,39 @@ type UserInput = {|
|
|
|
39
47
|
type Rubric = PerseusRadioWidgetOptions;
|
|
40
48
|
type Props = WidgetProps<RenderProps, Rubric>;
|
|
41
49
|
|
|
50
|
+
type DefaultProps = {|
|
|
51
|
+
choices: Props["choices"],
|
|
52
|
+
multipleSelect: Props["multipleSelect"],
|
|
53
|
+
countChoices: Props["countChoices"],
|
|
54
|
+
deselectEnabled: Props["deselectEnabled"],
|
|
55
|
+
linterContext: Props["linterContext"],
|
|
56
|
+
|};
|
|
57
|
+
|
|
42
58
|
class Radio extends React.Component<Props> {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
focusFunction: FocusFunction;
|
|
60
|
+
|
|
61
|
+
static defaultProps: DefaultProps = {
|
|
62
|
+
choices: [],
|
|
46
63
|
multipleSelect: false,
|
|
47
64
|
countChoices: false,
|
|
48
65
|
deselectEnabled: false,
|
|
49
66
|
linterContext: linterContextDefault,
|
|
50
67
|
};
|
|
51
68
|
|
|
52
|
-
_renderRenderer: (string) => React.Node = (
|
|
53
|
-
content =
|
|
54
|
-
|
|
69
|
+
_renderRenderer: (content?: string) => React.Node = (
|
|
70
|
+
content?: string = "",
|
|
71
|
+
) => {
|
|
55
72
|
let nextPassageRefId = 1;
|
|
56
73
|
const widgets = {};
|
|
57
74
|
|
|
58
75
|
const modContent = content.replace(
|
|
59
76
|
/\{\{passage-ref (\d+) (\d+)(?: "([^"]*)")?\}\}/g,
|
|
60
|
-
(
|
|
77
|
+
(
|
|
78
|
+
match: string,
|
|
79
|
+
passageNum: string,
|
|
80
|
+
refNum: string,
|
|
81
|
+
summaryText: string,
|
|
82
|
+
) => {
|
|
61
83
|
const widgetId = "passage-ref " + nextPassageRefId;
|
|
62
84
|
nextPassageRefId++;
|
|
63
85
|
|
|
@@ -105,10 +127,19 @@ class Radio extends React.Component<Props> {
|
|
|
105
127
|
// that we pass a value to `.focus()` and it seems to have been used for
|
|
106
128
|
// adding hints when editing.
|
|
107
129
|
// See: https://github.com/Khan/perseus/blame/e18582b4b69959270b90e237ef1813899711ddfa/src/widgets/radio.js#L169
|
|
108
|
-
focus:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
130
|
+
focus(choiceIndex: ?number): boolean {
|
|
131
|
+
if (this.focusFunction) {
|
|
132
|
+
return this.focusFunction(choiceIndex);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// lets BaseRadio regiser a focus callback so widget
|
|
139
|
+
// can focus an individual choice
|
|
140
|
+
registerFocusFunction(fun: FocusFunction): void {
|
|
141
|
+
this.focusFunction = fun;
|
|
142
|
+
}
|
|
112
143
|
|
|
113
144
|
// When `BaseRadio`'s `onChange` handler is called, indicating a change in
|
|
114
145
|
// our choices' state, we need to call our `onChange` handler in order to
|
|
@@ -123,7 +154,12 @@ class Radio extends React.Component<Props> {
|
|
|
123
154
|
//
|
|
124
155
|
// NOTE(mdr): This method expects to be auto-bound. If this component is
|
|
125
156
|
// converted to an ES6 class, take care to auto-bind this method!
|
|
126
|
-
updateChoices: (
|
|
157
|
+
updateChoices: (
|
|
158
|
+
newValueLists: $ReadOnly<{
|
|
159
|
+
checked: $ReadOnlyArray<boolean>,
|
|
160
|
+
crossedOut: $ReadOnlyArray<boolean>,
|
|
161
|
+
}>,
|
|
162
|
+
) => void = (newValueLists) => {
|
|
127
163
|
const {choiceStates, choices} = this.props;
|
|
128
164
|
|
|
129
165
|
// Construct the baseline `choiceStates` objects. If this is the user's
|
|
@@ -131,24 +167,21 @@ class Radio extends React.Component<Props> {
|
|
|
131
167
|
// new objects with all fields set to the default values. Otherwise, we
|
|
132
168
|
// should clone the old `choiceStates` objects, in preparation to
|
|
133
169
|
// mutate them.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
readOnly: false,
|
|
146
|
-
}));
|
|
147
|
-
}
|
|
170
|
+
const newChoiceStates = choiceStates
|
|
171
|
+
? choiceStates.map((state: ChoiceState) => ({...state}))
|
|
172
|
+
: choices.map(() => ({
|
|
173
|
+
selected: false,
|
|
174
|
+
crossedOut: false,
|
|
175
|
+
highlighted: false,
|
|
176
|
+
rationaleShown: false,
|
|
177
|
+
correctnessShown: false,
|
|
178
|
+
previouslyAnswered: false,
|
|
179
|
+
readOnly: false,
|
|
180
|
+
}));
|
|
148
181
|
|
|
149
182
|
// Mutate the new `choiceState` objects, according to the new `checked`
|
|
150
183
|
// and `crossedOut` values provided in `newValueLists`.
|
|
151
|
-
newChoiceStates.forEach((choiceState, i) => {
|
|
184
|
+
newChoiceStates.forEach((choiceState: ChoiceState, i) => {
|
|
152
185
|
choiceState.selected = newValueLists.checked[i];
|
|
153
186
|
choiceState.crossedOut = newValueLists.crossedOut[i];
|
|
154
187
|
});
|
|
@@ -178,25 +211,29 @@ class Radio extends React.Component<Props> {
|
|
|
178
211
|
const widgetCorrect =
|
|
179
212
|
score.type === "points" && score.total === score.earned;
|
|
180
213
|
|
|
181
|
-
const newStates = choiceStates.map(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
214
|
+
const newStates: $ReadOnlyArray<ChoiceState> = choiceStates.map(
|
|
215
|
+
(state: ChoiceState): ChoiceState => ({
|
|
216
|
+
...state,
|
|
217
|
+
highlighted: state.selected,
|
|
218
|
+
// If the choice is selected, show the rationale now
|
|
219
|
+
rationaleShown:
|
|
220
|
+
state.selected ||
|
|
221
|
+
// If the choice already had a rationale, keep it shown
|
|
222
|
+
state.rationaleShown ||
|
|
223
|
+
// If the widget is correctly answered, show the rationale
|
|
224
|
+
// for all the choices
|
|
225
|
+
widgetCorrect,
|
|
226
|
+
// We use the same behavior for the readOnly flag as for
|
|
227
|
+
// rationaleShown, but we keep it separate in case other
|
|
228
|
+
// behaviors want to disable choices without showing rationales.
|
|
229
|
+
readOnly:
|
|
230
|
+
state.selected || state.readOnly || widgetCorrect,
|
|
231
|
+
correctnessShown:
|
|
232
|
+
state.selected || state.correctnessShown,
|
|
233
|
+
previouslyAnswered:
|
|
234
|
+
state.previouslyAnswered || state.selected,
|
|
235
|
+
}),
|
|
236
|
+
);
|
|
200
237
|
|
|
201
238
|
this.props.onChange(
|
|
202
239
|
{
|
|
@@ -213,11 +250,15 @@ class Radio extends React.Component<Props> {
|
|
|
213
250
|
*/
|
|
214
251
|
deselectIncorrectSelectedChoices: () => void = () => {
|
|
215
252
|
if (this.props.choiceStates) {
|
|
216
|
-
const newStates =
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
253
|
+
const newStates: $ReadOnlyArray<ChoiceState> =
|
|
254
|
+
this.props.choiceStates.map(
|
|
255
|
+
(state: ChoiceState, i): ChoiceState => ({
|
|
256
|
+
...state,
|
|
257
|
+
selected:
|
|
258
|
+
state.selected && !!this.props.choices[i].correct,
|
|
259
|
+
highlighted: false,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
221
262
|
|
|
222
263
|
this.props.onChange(
|
|
223
264
|
{
|
|
@@ -230,12 +271,12 @@ class Radio extends React.Component<Props> {
|
|
|
230
271
|
};
|
|
231
272
|
|
|
232
273
|
render(): React.Node {
|
|
233
|
-
|
|
234
|
-
let choiceStates
|
|
274
|
+
const {choices} = this.props;
|
|
275
|
+
let choiceStates: $ReadOnlyArray<ChoiceState>;
|
|
235
276
|
if (this.props.static) {
|
|
236
|
-
choiceStates =
|
|
237
|
-
selected:
|
|
238
|
-
crossedOut:
|
|
277
|
+
choiceStates = choices.map((choice) => ({
|
|
278
|
+
selected: !!choice.correct,
|
|
279
|
+
crossedOut: false,
|
|
239
280
|
readOnly: true,
|
|
240
281
|
highlighted: false,
|
|
241
282
|
rationaleShown: true,
|
|
@@ -244,11 +285,10 @@ class Radio extends React.Component<Props> {
|
|
|
244
285
|
}));
|
|
245
286
|
} else if (this.props.choiceStates) {
|
|
246
287
|
choiceStates = this.props.choiceStates;
|
|
247
|
-
// $FlowFixMe[prop-missing]
|
|
248
288
|
} else if (this.props.values) {
|
|
249
289
|
// Support legacy choiceStates implementation
|
|
250
290
|
/* istanbul ignore next - props.values is deprecated */
|
|
251
|
-
choiceStates =
|
|
291
|
+
choiceStates = this.props.values.map((val) => ({
|
|
252
292
|
selected: val,
|
|
253
293
|
crossedOut: false,
|
|
254
294
|
readOnly: false,
|
|
@@ -258,7 +298,7 @@ class Radio extends React.Component<Props> {
|
|
|
258
298
|
previouslyAnswered: false,
|
|
259
299
|
}));
|
|
260
300
|
} else {
|
|
261
|
-
choiceStates =
|
|
301
|
+
choiceStates = choices.map(() => ({
|
|
262
302
|
selected: false,
|
|
263
303
|
crossedOut: false,
|
|
264
304
|
readOnly: false,
|
|
@@ -269,80 +309,82 @@ class Radio extends React.Component<Props> {
|
|
|
269
309
|
}));
|
|
270
310
|
}
|
|
271
311
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
312
|
+
const choicesProp: $ReadOnlyArray<ChoiceType> = choices.map(
|
|
313
|
+
(choice, i) => {
|
|
314
|
+
const content =
|
|
315
|
+
choice.isNoneOfTheAbove && !choice.content
|
|
316
|
+
? // we use i18n._ instead of $_ here because the content
|
|
317
|
+
// sent to a renderer needs to be a string, not a react
|
|
318
|
+
// node (/renderable/fragment).
|
|
319
|
+
i18n._("None of the above")
|
|
320
|
+
: choice.content;
|
|
321
|
+
|
|
322
|
+
const {
|
|
323
|
+
selected,
|
|
324
|
+
crossedOut,
|
|
325
|
+
rationaleShown,
|
|
326
|
+
correctnessShown,
|
|
327
|
+
readOnly,
|
|
328
|
+
highlighted,
|
|
329
|
+
previouslyAnswered,
|
|
330
|
+
} = choiceStates[i];
|
|
331
|
+
|
|
332
|
+
const reviewChoice =
|
|
333
|
+
this.props.reviewModeRubric &&
|
|
334
|
+
this.props.reviewModeRubric.choices[i];
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
content: this._renderRenderer(content),
|
|
338
|
+
checked: selected,
|
|
339
|
+
// Current versions of the radio widget always pass in the
|
|
340
|
+
// "correct" value through the choices. Old serialized state
|
|
341
|
+
// for radio widgets doesn't have this though, so we have to
|
|
342
|
+
// pull the correctness out of the review mode rubric. This
|
|
343
|
+
// only works because all of the places we use
|
|
344
|
+
// `restoreSerializedState()` also turn on reviewMode, but is
|
|
345
|
+
// fine for now.
|
|
346
|
+
// TODO(emily): Come up with a more comprehensive way to solve
|
|
347
|
+
// this sort of "serialized state breaks when internal
|
|
348
|
+
// structure changes" problem.
|
|
349
|
+
correct:
|
|
350
|
+
choice.correct === undefined
|
|
351
|
+
? !!reviewChoice && !!reviewChoice.correct
|
|
352
|
+
: choice.correct,
|
|
353
|
+
disabled: readOnly,
|
|
354
|
+
hasRationale: !!choice.clue,
|
|
355
|
+
rationale: this._renderRenderer(choice.clue),
|
|
356
|
+
showRationale: rationaleShown,
|
|
357
|
+
showCorrectness: correctnessShown,
|
|
358
|
+
isNoneOfTheAbove: !!choice.isNoneOfTheAbove,
|
|
359
|
+
revealNoneOfTheAbove: !!(
|
|
360
|
+
this.props.questionCompleted && selected
|
|
361
|
+
),
|
|
362
|
+
crossedOut,
|
|
363
|
+
highlighted,
|
|
364
|
+
previouslyAnswered: previouslyAnswered,
|
|
365
|
+
};
|
|
366
|
+
},
|
|
367
|
+
);
|
|
324
368
|
|
|
325
369
|
return (
|
|
326
370
|
<BaseRadio
|
|
327
|
-
// eslint-disable-next-line react/no-string-refs
|
|
328
|
-
ref="baseRadio"
|
|
329
371
|
labelWrap={true}
|
|
330
372
|
multipleSelect={this.props.multipleSelect}
|
|
331
373
|
countChoices={this.props.countChoices}
|
|
332
374
|
numCorrect={this.props.numCorrect}
|
|
333
|
-
choices={
|
|
375
|
+
choices={choicesProp}
|
|
334
376
|
onChange={this.updateChoices}
|
|
335
377
|
reviewModeRubric={this.props.reviewModeRubric}
|
|
336
378
|
deselectEnabled={this.props.deselectEnabled}
|
|
337
379
|
apiOptions={this.props.apiOptions}
|
|
338
380
|
isLastUsedWidget={this.props.isLastUsedWidget}
|
|
381
|
+
registerFocusFunction={(i) => this.registerFocusFunction(i)}
|
|
339
382
|
/>
|
|
340
383
|
);
|
|
341
384
|
}
|
|
342
385
|
|
|
343
386
|
static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
|
|
344
|
-
const numSelected =
|
|
345
|
-
userInput.choicesSelected,
|
|
387
|
+
const numSelected = userInput.choicesSelected.reduce(
|
|
346
388
|
(sum, selected) => {
|
|
347
389
|
return sum + (selected ? 1 : 0);
|
|
348
390
|
},
|
|
@@ -355,8 +397,12 @@ class Radio extends React.Component<Props> {
|
|
|
355
397
|
message: null,
|
|
356
398
|
};
|
|
357
399
|
}
|
|
358
|
-
|
|
359
|
-
if (
|
|
400
|
+
|
|
401
|
+
if (
|
|
402
|
+
userInput.numCorrect &&
|
|
403
|
+
userInput.numCorrect > 1 &&
|
|
404
|
+
numSelected !== userInput.numCorrect
|
|
405
|
+
) {
|
|
360
406
|
return {
|
|
361
407
|
type: "invalid",
|
|
362
408
|
message: i18n._("Please choose the correct number of answers."),
|
|
@@ -372,11 +418,11 @@ class Radio extends React.Component<Props> {
|
|
|
372
418
|
),
|
|
373
419
|
};
|
|
374
420
|
}
|
|
375
|
-
|
|
376
|
-
const correct =
|
|
421
|
+
|
|
422
|
+
const correct = userInput.choicesSelected.every((selected, i) => {
|
|
377
423
|
let isCorrect;
|
|
378
424
|
if (userInput.noneOfTheAboveIndex === i) {
|
|
379
|
-
isCorrect =
|
|
425
|
+
isCorrect = rubric.choices.every((choice, j) => {
|
|
380
426
|
return i === j || !choice.correct;
|
|
381
427
|
});
|
|
382
428
|
} else {
|
|
@@ -384,7 +430,6 @@ class Radio extends React.Component<Props> {
|
|
|
384
430
|
}
|
|
385
431
|
return isCorrect === selected;
|
|
386
432
|
});
|
|
387
|
-
/* jshint +W018 */
|
|
388
433
|
|
|
389
434
|
return {
|
|
390
435
|
type: "points",
|
|
@@ -408,9 +453,8 @@ class Radio extends React.Component<Props> {
|
|
|
408
453
|
const numCorrect = props.numCorrect;
|
|
409
454
|
|
|
410
455
|
for (let i = 0; i < choicesSelected.length; i++) {
|
|
411
|
-
// $FlowFixMe[prop-missing]
|
|
412
456
|
const index = props.choices[i].originalIndex;
|
|
413
|
-
|
|
457
|
+
|
|
414
458
|
choicesSelected[index] = choiceStates[i].selected;
|
|
415
459
|
|
|
416
460
|
if (props.choices[i].isNoneOfTheAbove) {
|
|
@@ -432,28 +476,29 @@ class Radio extends React.Component<Props> {
|
|
|
432
476
|
// Support legacy choiceState implementation
|
|
433
477
|
}
|
|
434
478
|
/* istanbul ignore if - props.values is deprecated */
|
|
435
|
-
|
|
436
|
-
if (
|
|
479
|
+
const {values} = props;
|
|
480
|
+
if (values) {
|
|
437
481
|
let noneOfTheAboveIndex = null;
|
|
438
482
|
let noneOfTheAboveSelected = false;
|
|
439
483
|
|
|
440
|
-
const
|
|
484
|
+
const choicesSelected = [...values];
|
|
441
485
|
const countChoices = props.countChoices;
|
|
442
486
|
const numCorrect = props.numCorrect;
|
|
487
|
+
const valuesLength = values.length;
|
|
443
488
|
|
|
444
|
-
for (let i = 0; i <
|
|
489
|
+
for (let i = 0; i < valuesLength; i++) {
|
|
445
490
|
const index = props.choices[i].originalIndex;
|
|
446
|
-
|
|
491
|
+
choicesSelected[index] = values[i];
|
|
447
492
|
|
|
448
493
|
if (props.choices[i].isNoneOfTheAbove) {
|
|
449
494
|
noneOfTheAboveIndex = index;
|
|
450
|
-
if (
|
|
495
|
+
if (choicesSelected[i]) {
|
|
451
496
|
noneOfTheAboveSelected = true;
|
|
452
497
|
}
|
|
453
498
|
}
|
|
454
499
|
}
|
|
455
500
|
return {
|
|
456
|
-
choicesSelected
|
|
501
|
+
choicesSelected,
|
|
457
502
|
noneOfTheAboveIndex,
|
|
458
503
|
noneOfTheAboveSelected,
|
|
459
504
|
countChoices,
|
|
@@ -462,7 +507,7 @@ class Radio extends React.Component<Props> {
|
|
|
462
507
|
}
|
|
463
508
|
// Nothing checked
|
|
464
509
|
return {
|
|
465
|
-
choicesSelected:
|
|
510
|
+
choicesSelected: props.choices.map(() => false),
|
|
466
511
|
};
|
|
467
512
|
}
|
|
468
513
|
}
|
package/src/widgets/radio.jsx
CHANGED
|
@@ -6,11 +6,8 @@ import Util from "../util.js";
|
|
|
6
6
|
|
|
7
7
|
import Radio from "./radio/widget.jsx";
|
|
8
8
|
|
|
9
|
-
import type {
|
|
10
|
-
|
|
11
|
-
PerseusRadioChoice,
|
|
12
|
-
} from "../perseus-types.js";
|
|
13
|
-
import type {WidgetExports} from "../types.js";
|
|
9
|
+
import type {PerseusRadioWidgetOptions} from "../perseus-types.js";
|
|
10
|
+
import type {WidgetExports, RadioChoiceWithMetadata} from "../types.js";
|
|
14
11
|
import type {RenderProps} from "./radio/widget.jsx";
|
|
15
12
|
|
|
16
13
|
const {shuffle, random} = Util;
|
|
@@ -43,15 +40,16 @@ const _choiceTransform = (
|
|
|
43
40
|
};
|
|
44
41
|
|
|
45
42
|
const _addNoneOfAbove = function (
|
|
46
|
-
choices: $ReadOnlyArray<
|
|
47
|
-
)
|
|
43
|
+
choices: $ReadOnlyArray<RadioChoiceWithMetadata>,
|
|
44
|
+
) {
|
|
48
45
|
let noneOfTheAbove = null;
|
|
49
46
|
|
|
50
|
-
const newChoices =
|
|
47
|
+
const newChoices = choices.filter((choice, index) => {
|
|
51
48
|
if (choice.isNoneOfTheAbove) {
|
|
52
49
|
noneOfTheAbove = choice;
|
|
53
|
-
return
|
|
50
|
+
return false;
|
|
54
51
|
}
|
|
52
|
+
return true;
|
|
55
53
|
});
|
|
56
54
|
|
|
57
55
|
// Place the "None of the above" options last
|
|
@@ -63,8 +61,8 @@ const _choiceTransform = (
|
|
|
63
61
|
};
|
|
64
62
|
|
|
65
63
|
const enforceOrdering = (
|
|
66
|
-
choices: $ReadOnlyArray<
|
|
67
|
-
)
|
|
64
|
+
choices: $ReadOnlyArray<RadioChoiceWithMetadata>,
|
|
65
|
+
) => {
|
|
68
66
|
const content = choices.map((c) => c.content);
|
|
69
67
|
if (ReversedChoices.some((reversed) => _.isEqual(content, reversed))) {
|
|
70
68
|
return [choices[1], choices[0]];
|
|
@@ -73,13 +71,14 @@ const _choiceTransform = (
|
|
|
73
71
|
};
|
|
74
72
|
|
|
75
73
|
// Add meta-information to choices
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
74
|
+
const choices: $ReadOnlyArray<RadioChoiceWithMetadata> =
|
|
75
|
+
widgetOptions.choices.map((choice, i): RadioChoiceWithMetadata => {
|
|
76
|
+
return {
|
|
77
|
+
...choice,
|
|
78
|
+
originalIndex: i,
|
|
79
|
+
correct: Boolean(choice.correct),
|
|
80
|
+
};
|
|
81
81
|
});
|
|
82
|
-
});
|
|
83
82
|
|
|
84
83
|
// Apply all the transforms. Note that the order we call these is
|
|
85
84
|
// important!
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
// @flow
|
|
2
|
-
import colors from "@khanacademy/wonder-blocks-color";
|
|
3
|
-
import * as React from "react";
|
|
4
|
-
|
|
5
|
-
import CrossOutButton from "../cross-out-button.jsx";
|
|
6
|
-
|
|
7
|
-
type StoryArgs = {||};
|
|
8
|
-
|
|
9
|
-
type Story = {|
|
|
10
|
-
title: string,
|
|
11
|
-
|};
|
|
12
|
-
|
|
13
|
-
export default ({
|
|
14
|
-
title: "Perseus/Widgets/Radio/Cross Out Button",
|
|
15
|
-
}: Story);
|
|
16
|
-
|
|
17
|
-
export const CrossOutButtonForChoiceAColoredDefaultKaGreen = (
|
|
18
|
-
args: StoryArgs,
|
|
19
|
-
): React.Node => {
|
|
20
|
-
return (
|
|
21
|
-
<CrossOutButton
|
|
22
|
-
onClick={() => {}}
|
|
23
|
-
onFocus={() => {}}
|
|
24
|
-
onBlur={() => {}}
|
|
25
|
-
pos={0}
|
|
26
|
-
crossedOut={false}
|
|
27
|
-
usingKeyboardFocus={false}
|
|
28
|
-
theme="day"
|
|
29
|
-
/>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const ReEnableButtonForChoiceCColoredWonderBlocksBlue = (
|
|
34
|
-
args: StoryArgs,
|
|
35
|
-
): React.Node => {
|
|
36
|
-
return (
|
|
37
|
-
<CrossOutButton
|
|
38
|
-
onClick={() => {}}
|
|
39
|
-
onFocus={() => {}}
|
|
40
|
-
onBlur={() => {}}
|
|
41
|
-
pos={0}
|
|
42
|
-
primaryProductColor={colors.blue}
|
|
43
|
-
crossedOut={true}
|
|
44
|
-
usingKeyboardFocus={false}
|
|
45
|
-
theme="day"
|
|
46
|
-
/>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
export const CrossOutButtonForChoiceAColoredWhiteForNightTheme = (
|
|
51
|
-
args: StoryArgs,
|
|
52
|
-
): React.Node => {
|
|
53
|
-
return (
|
|
54
|
-
<CrossOutButton
|
|
55
|
-
onClick={() => {}}
|
|
56
|
-
onFocus={() => {}}
|
|
57
|
-
onBlur={() => {}}
|
|
58
|
-
pos={0}
|
|
59
|
-
crossedOut={false}
|
|
60
|
-
usingKeyboardFocus={false}
|
|
61
|
-
theme="night"
|
|
62
|
-
/>
|
|
63
|
-
);
|
|
64
|
-
};
|