@khanacademy/perseus 1.0.0 → 1.1.1
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/CHANGELOG.md +19 -0
- package/dist/es/index.css +17 -8
- package/dist/es/index.js +258 -223
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +17 -8
- package/dist/index.js +261 -225
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/__stories__/input-with-examples.stories.jsx +1 -0
- package/src/components/input-with-examples.jsx +41 -24
- package/src/components/number-input.jsx +2 -2
- package/src/components/simple-keypad-input.jsx +18 -21
- package/src/components/text-input.jsx +5 -3
- package/src/index.js +1 -1
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +356 -65
- package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
- package/src/perseus-types.js +18 -7
- package/src/styles/perseus-renderer.less +8 -2
- package/src/styles/widgets/matrix.less +2 -0
- package/src/styles/widgets/passage.less +9 -5
- package/src/util/answer-types.js +2 -3
- package/src/util/math.js +3 -10
- package/src/widgets/__stories__/numeric-input.stories.jsx +94 -7
- package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +15 -6
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +5 -2
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +4 -1
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +228 -67
- package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +24 -24
- package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +2581 -679
- 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 +55 -7
- package/src/widgets/__tests__/radio/choice_test.jsx +164 -63
- package/src/widgets/__tests__/radio_test.jsx +39 -15
- package/src/widgets/definition.jsx +1 -1
- package/src/widgets/graded-group.jsx +5 -1
- package/src/widgets/group.jsx +84 -102
- package/src/widgets/input-number.jsx +6 -1
- package/src/widgets/matrix.jsx +17 -1
- package/src/widgets/numeric-input.jsx +99 -86
- package/src/widgets/radio/base-radio.jsx +3 -1
- package/src/widgets/radio/choice-a11y-text.js +62 -0
- package/src/widgets/radio/choice-icon/choice-icon.jsx +0 -68
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +5 -10
- package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +3 -12
- package/src/widgets/radio/choice.jsx +46 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
5
5
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
6
|
-
import
|
|
6
|
+
import {StyleSheet} from "aphrodite";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import _ from "underscore";
|
|
9
9
|
|
|
@@ -16,10 +16,17 @@ import KhanAnswerTypes from "../util/answer-types.js";
|
|
|
16
16
|
import KhanMath from "../util/math.js";
|
|
17
17
|
|
|
18
18
|
import type {
|
|
19
|
-
|
|
19
|
+
MathFormat,
|
|
20
20
|
PerseusNumericInputAnswer,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
PerseusNumericInputWidgetOptions,
|
|
22
|
+
PerseusNumericInputAnswerForm,
|
|
23
|
+
} from "../perseus-types";
|
|
24
|
+
import type {
|
|
25
|
+
FocusPath,
|
|
26
|
+
PerseusScore,
|
|
27
|
+
WidgetExports,
|
|
28
|
+
WidgetProps,
|
|
29
|
+
} from "../types";
|
|
23
30
|
|
|
24
31
|
const ParseTex = TexWrangler.parseTex;
|
|
25
32
|
|
|
@@ -36,7 +43,7 @@ const answerFormButtons = [
|
|
|
36
43
|
{title: "Numbers with \u03C0", value: "pi", content: "\u03C0"},
|
|
37
44
|
];
|
|
38
45
|
|
|
39
|
-
const formExamples = {
|
|
46
|
+
const formExamples: {[string]: (PerseusNumericInputAnswerForm) => string} = {
|
|
40
47
|
integer: () => i18n._("an integer, like $6$"),
|
|
41
48
|
proper: (form) =>
|
|
42
49
|
form.simplify === "optional"
|
|
@@ -95,6 +102,8 @@ type State = {|
|
|
|
95
102
|
|};
|
|
96
103
|
|
|
97
104
|
export class NumericInput extends React.Component<Props, State> {
|
|
105
|
+
inputRef: ?(SimpleKeypadInput | InputWithExamples);
|
|
106
|
+
|
|
98
107
|
static defaultProps: DefaultProps = {
|
|
99
108
|
currentValue: "",
|
|
100
109
|
size: "normal",
|
|
@@ -113,14 +122,11 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
113
122
|
};
|
|
114
123
|
}
|
|
115
124
|
|
|
116
|
-
static getOneCorrectAnswerFromRubric(
|
|
117
|
-
rubric
|
|
118
|
-
): ?PerseusNumericInputAnswer {
|
|
119
|
-
const correctAnswers = _.filter(
|
|
120
|
-
rubric.answers,
|
|
125
|
+
static getOneCorrectAnswerFromRubric(rubric: Rubric): ?string {
|
|
126
|
+
const correctAnswers = rubric.answers.filter(
|
|
121
127
|
(answer) => answer.status === "correct",
|
|
122
128
|
);
|
|
123
|
-
const answerStrings =
|
|
129
|
+
const answerStrings = correctAnswers.map((answer) => {
|
|
124
130
|
// Figure out how this answer is supposed to be
|
|
125
131
|
// displayed
|
|
126
132
|
let format = "decimal";
|
|
@@ -147,27 +153,36 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
static validate(useInput: UserInput, rubric: Rubric): PerseusScore {
|
|
150
|
-
const allAnswerForms =
|
|
151
|
-
|
|
152
|
-
const createValidator = (answer) =>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
156
|
+
const allAnswerForms = answerFormButtons.map((e) => e["value"]);
|
|
157
|
+
|
|
158
|
+
const createValidator = (answer: PerseusNumericInputAnswer) => {
|
|
159
|
+
const stringAnswer = `${answer.value}`;
|
|
160
|
+
return KhanAnswerTypes.number.createValidatorFunctional(
|
|
161
|
+
stringAnswer,
|
|
162
|
+
{
|
|
163
|
+
message: answer.message,
|
|
164
|
+
simplify:
|
|
165
|
+
answer.status === "correct"
|
|
166
|
+
? answer.simplify
|
|
167
|
+
: "optional",
|
|
168
|
+
inexact: true, // TODO(merlob) backfill / delete
|
|
169
|
+
maxError: answer.maxError,
|
|
170
|
+
forms:
|
|
171
|
+
answer.strict &&
|
|
172
|
+
answer.answerForms &&
|
|
173
|
+
answer.answerForms.length !== 0
|
|
174
|
+
? answer.answerForms
|
|
175
|
+
: allAnswerForms,
|
|
176
|
+
},
|
|
177
|
+
);
|
|
178
|
+
};
|
|
166
179
|
|
|
167
180
|
// We may have received TeX; try to parse it before grading.
|
|
168
181
|
// If `currentValue` is not TeX, this should be a no-op.
|
|
169
182
|
const currentValue = ParseTex(useInput.currentValue);
|
|
170
|
-
const correctAnswers =
|
|
183
|
+
const correctAnswers = rubric.answers.filter(
|
|
184
|
+
(answer) => answer.status === "correct",
|
|
185
|
+
);
|
|
171
186
|
|
|
172
187
|
const normalizedAnswerExpected = correctAnswers.every(
|
|
173
188
|
(answer) => Math.abs(answer.value) <= 1,
|
|
@@ -177,8 +192,8 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
177
192
|
// precisely or approximately and return the appropriate message:
|
|
178
193
|
// - if precise, return the message that the answer came with
|
|
179
194
|
// - if it needs to be simplified, etc., show that message
|
|
180
|
-
let result =
|
|
181
|
-
|
|
195
|
+
let result = correctAnswers
|
|
196
|
+
.map((answer) => {
|
|
182
197
|
// The coefficient is an attribute of the widget
|
|
183
198
|
let localValue = currentValue;
|
|
184
199
|
if (rubric.coefficient) {
|
|
@@ -195,20 +210,19 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
195
210
|
normalizedAnswerExpected,
|
|
196
211
|
),
|
|
197
212
|
);
|
|
198
|
-
})
|
|
199
|
-
(match) => match.correct || match.empty
|
|
200
|
-
);
|
|
213
|
+
})
|
|
214
|
+
.find((match) => match.correct || match.empty);
|
|
201
215
|
|
|
202
216
|
if (!result) {
|
|
203
217
|
// Otherwise, if the guess is not correct
|
|
204
218
|
const otherAnswers = [].concat(
|
|
205
|
-
|
|
206
|
-
|
|
219
|
+
rubric.answers.filter((answer) => answer.status === "ungraded"),
|
|
220
|
+
rubric.answers.filter((answer) => answer.status === "wrong"),
|
|
207
221
|
);
|
|
208
222
|
|
|
209
223
|
// Look through all other answers and if one matches either
|
|
210
224
|
// precisely or approximately return the answer's message
|
|
211
|
-
const match =
|
|
225
|
+
const match = otherAnswers.find((answer) => {
|
|
212
226
|
const validate = createValidator(answer);
|
|
213
227
|
return validate(
|
|
214
228
|
maybeParsePercentInput(
|
|
@@ -259,14 +273,13 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
259
273
|
correct = score.type === "points" && score.earned === score.total;
|
|
260
274
|
|
|
261
275
|
if (!correct) {
|
|
262
|
-
const correctAnswers =
|
|
263
|
-
rubric.answers,
|
|
276
|
+
const correctAnswers = rubric.answers.filter(
|
|
264
277
|
(answer) => answer.status === "correct",
|
|
265
278
|
);
|
|
266
|
-
const answerStrings =
|
|
279
|
+
const answerStrings = correctAnswers.map((answer) => {
|
|
267
280
|
// Figure out how this answer is supposed to be
|
|
268
281
|
// displayed
|
|
269
|
-
let format = "decimal";
|
|
282
|
+
let format: MathFormat = "decimal";
|
|
270
283
|
if (answer.answerForms && answer.answerForms[0]) {
|
|
271
284
|
// NOTE(johnsullivan): This isn't exactly ideal, but
|
|
272
285
|
// it does behave well for all the currently known
|
|
@@ -298,7 +311,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
298
311
|
const forms =
|
|
299
312
|
this.props.answerForms?.length !== 0
|
|
300
313
|
? this.props.answerForms
|
|
301
|
-
:
|
|
314
|
+
: Object.keys(formExamples).map((name) => {
|
|
302
315
|
return {
|
|
303
316
|
name: name,
|
|
304
317
|
simplify: "required",
|
|
@@ -320,10 +333,11 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
320
333
|
// To check if all answer forms are accepted, we must first
|
|
321
334
|
// find the *names* of all accepted forms, and see if they are
|
|
322
335
|
// all present, ignoring duplicates
|
|
323
|
-
const answerFormNames = _.uniq(
|
|
336
|
+
const answerFormNames: $ReadOnlyArray<string> = _.uniq(
|
|
324
337
|
this.props.answerForms?.map((form) => form.name),
|
|
325
338
|
);
|
|
326
|
-
const allFormsAccepted =
|
|
339
|
+
const allFormsAccepted =
|
|
340
|
+
answerFormNames.length >= Object.keys(formExamples).length;
|
|
327
341
|
return !noFormsAccepted && !allFormsAccepted;
|
|
328
342
|
};
|
|
329
343
|
|
|
@@ -332,20 +346,16 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
332
346
|
};
|
|
333
347
|
|
|
334
348
|
focus: () => boolean = () => {
|
|
335
|
-
|
|
336
|
-
this.refs.input.focus();
|
|
349
|
+
this.inputRef?.focus();
|
|
337
350
|
return true;
|
|
338
351
|
};
|
|
339
352
|
|
|
340
|
-
focusInputPath: (
|
|
341
|
-
|
|
342
|
-
// eslint-disable-next-line react/no-string-refs
|
|
343
|
-
this.refs.input.focus();
|
|
353
|
+
focusInputPath: () => void = () => {
|
|
354
|
+
this.inputRef?.focus();
|
|
344
355
|
};
|
|
345
356
|
|
|
346
|
-
blurInputPath: (
|
|
347
|
-
|
|
348
|
-
this.refs.input.blur();
|
|
357
|
+
blurInputPath: () => void = () => {
|
|
358
|
+
this.inputRef?.blur();
|
|
349
359
|
};
|
|
350
360
|
|
|
351
361
|
getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
|
|
@@ -355,12 +365,12 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
355
365
|
return [[]];
|
|
356
366
|
};
|
|
357
367
|
|
|
358
|
-
getGrammarTypeForPath: (
|
|
368
|
+
getGrammarTypeForPath: (FocusPath) => string = (inputPath) => {
|
|
359
369
|
/* istanbul ignore next */
|
|
360
370
|
return "number";
|
|
361
371
|
};
|
|
362
372
|
|
|
363
|
-
setInputValue: (
|
|
373
|
+
setInputValue: (FocusPath, string, ?() => mixed) => void = (
|
|
364
374
|
path,
|
|
365
375
|
newValue,
|
|
366
376
|
cb,
|
|
@@ -378,7 +388,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
378
388
|
return NumericInput.getUserInputFromProps(this.props);
|
|
379
389
|
};
|
|
380
390
|
|
|
381
|
-
handleChange: (string,
|
|
391
|
+
handleChange: (string, ?() => mixed) => void = (newValue, cb) => {
|
|
382
392
|
this.props.onChange({currentValue: newValue}, cb);
|
|
383
393
|
this.props.trackInteraction();
|
|
384
394
|
};
|
|
@@ -409,7 +419,6 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
409
419
|
const rubric = this.props.reviewModeRubric;
|
|
410
420
|
const answers = this.getAnswerBlurb(rubric);
|
|
411
421
|
const answerBlurb = answers[0];
|
|
412
|
-
const classes = [];
|
|
413
422
|
|
|
414
423
|
let labelText = this.props.labelText;
|
|
415
424
|
if (labelText == null || labelText === "") {
|
|
@@ -429,8 +438,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
429
438
|
// TODO(charlie): Support "Review Mode".
|
|
430
439
|
return maybeRightAlignKeypadInput(
|
|
431
440
|
<SimpleKeypadInput
|
|
432
|
-
|
|
433
|
-
ref="input"
|
|
441
|
+
ref={(ref) => (this.inputRef = ref)}
|
|
434
442
|
value={this.props.currentValue}
|
|
435
443
|
keypadElement={this.props.keypadElement}
|
|
436
444
|
onChange={this.handleChange}
|
|
@@ -440,13 +448,20 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
440
448
|
);
|
|
441
449
|
}
|
|
442
450
|
|
|
451
|
+
const styles = StyleSheet.create({
|
|
452
|
+
input: {
|
|
453
|
+
textAlign: this.props.rightAlign ? "right" : "left",
|
|
454
|
+
width: this.props.size === "small" ? 40 : 80,
|
|
455
|
+
padding: 0,
|
|
456
|
+
height: "auto",
|
|
457
|
+
},
|
|
458
|
+
});
|
|
459
|
+
|
|
443
460
|
const input = (
|
|
444
461
|
<InputWithExamples
|
|
445
|
-
|
|
446
|
-
ref="input"
|
|
462
|
+
ref={(ref) => (this.inputRef = ref)}
|
|
447
463
|
value={this.props.currentValue}
|
|
448
464
|
onChange={this.handleChange}
|
|
449
|
-
className={classNames(classes)}
|
|
450
465
|
labelText={labelText}
|
|
451
466
|
type={this._getInputType()}
|
|
452
467
|
examples={this.examples()}
|
|
@@ -455,8 +470,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
455
470
|
onBlur={this._handleBlur}
|
|
456
471
|
id={this.props.widgetId}
|
|
457
472
|
disabled={this.props.apiOptions.readOnly}
|
|
458
|
-
|
|
459
|
-
highlightLint={this.props.highlightLint}
|
|
473
|
+
style={styles.input}
|
|
460
474
|
/>
|
|
461
475
|
);
|
|
462
476
|
|
|
@@ -493,7 +507,9 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
493
507
|
// a given *problem* rather than for each possible [correct/wrong] *answer*.
|
|
494
508
|
// When should two answers to a problem take different answer types?
|
|
495
509
|
// See D27790 for more discussion.
|
|
496
|
-
export const unionAnswerForms: (
|
|
510
|
+
export const unionAnswerForms: (
|
|
511
|
+
$ReadOnlyArray<$ReadOnlyArray<PerseusNumericInputAnswerForm>>,
|
|
512
|
+
) => $ReadOnlyArray<PerseusNumericInputAnswerForm> = function (
|
|
497
513
|
answerFormsList,
|
|
498
514
|
) {
|
|
499
515
|
// Takes a list of lists of answer forms, and returns a list of the forms
|
|
@@ -504,28 +520,25 @@ export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
|
|
|
504
520
|
// two elements are equal, and returns a list of unique elements. This is
|
|
505
521
|
// just a helper function here, but works generally.
|
|
506
522
|
const uniqueBy = function (list, iteratee) {
|
|
507
|
-
return
|
|
508
|
-
list
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return uniqueList.concat([element]);
|
|
517
|
-
},
|
|
518
|
-
[],
|
|
519
|
-
);
|
|
523
|
+
return list.reduce((uniqueList, element) => {
|
|
524
|
+
// For each element, decide whether it's already in the list of
|
|
525
|
+
// unique items.
|
|
526
|
+
const inList = _.find(uniqueList, iteratee.bind(null, element));
|
|
527
|
+
if (inList) {
|
|
528
|
+
return uniqueList;
|
|
529
|
+
}
|
|
530
|
+
return uniqueList.concat([element]);
|
|
531
|
+
}, []);
|
|
520
532
|
};
|
|
521
533
|
|
|
522
534
|
// Pull out all of the forms from the different lists.
|
|
523
|
-
const allForms =
|
|
535
|
+
const allForms = answerFormsList.flat();
|
|
524
536
|
// Pull out the unique forms using uniqueBy.
|
|
525
537
|
const uniqueForms = uniqueBy(allForms, _.isEqual);
|
|
526
538
|
// Sort them by the order they appear in the `formExamples` list.
|
|
539
|
+
const formExampleKeys = Object.keys(formExamples);
|
|
527
540
|
return _.sortBy(uniqueForms, (form) => {
|
|
528
|
-
return
|
|
541
|
+
return formExampleKeys.indexOf(form.name);
|
|
529
542
|
});
|
|
530
543
|
};
|
|
531
544
|
|
|
@@ -535,7 +548,7 @@ type RenderProps = {|
|
|
|
535
548
|
name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi",
|
|
536
549
|
|}>,
|
|
537
550
|
labelText: string,
|
|
538
|
-
size:
|
|
551
|
+
size: "normal" | "small",
|
|
539
552
|
coefficient: boolean,
|
|
540
553
|
rightAlign?: boolean,
|
|
541
554
|
static: boolean,
|
|
@@ -547,10 +560,10 @@ type RenderProps = {|
|
|
|
547
560
|
// can accept several input forms with or without "%", the decision
|
|
548
561
|
// to parse based on the presence of "%" in the input, is so that we
|
|
549
562
|
// don't accidently scale the user typed value before grading, CP-930.
|
|
550
|
-
export
|
|
551
|
-
inputValue,
|
|
552
|
-
normalizedAnswerExpected,
|
|
553
|
-
)
|
|
563
|
+
export function maybeParsePercentInput(
|
|
564
|
+
inputValue: string | number,
|
|
565
|
+
normalizedAnswerExpected: boolean,
|
|
566
|
+
): string | number {
|
|
554
567
|
// If the input value is not a string ending with "%", then there's
|
|
555
568
|
// nothing more to do. The value will be graded as inputted by user.
|
|
556
569
|
if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
|
|
@@ -575,7 +588,7 @@ export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
|
|
|
575
588
|
|
|
576
589
|
// Otherwise, we return input valuåe (number) stripped of the "%".
|
|
577
590
|
return value;
|
|
578
|
-
}
|
|
591
|
+
}
|
|
579
592
|
|
|
580
593
|
const propsTransform = function (
|
|
581
594
|
widgetOptions: PerseusNumericInputWidgetOptions,
|
|
@@ -584,7 +597,7 @@ const propsTransform = function (
|
|
|
584
597
|
answerForms: unionAnswerForms(
|
|
585
598
|
// Pull out the name of each form and whether that form has
|
|
586
599
|
// required simplification.
|
|
587
|
-
|
|
600
|
+
widgetOptions.answers.map((answer) => {
|
|
588
601
|
return _.map(answer.answerForms, (form) => {
|
|
589
602
|
return {
|
|
590
603
|
simplify: answer.simplify,
|
|
@@ -249,7 +249,9 @@ function BaseRadio(props: Props): React.Node {
|
|
|
249
249
|
>
|
|
250
250
|
<legend className="perseus-sr-only">{instructions}</legend>
|
|
251
251
|
{shouldShowInstructions && (
|
|
252
|
-
<div className={instructionsClassName}>
|
|
252
|
+
<div className={instructionsClassName} aria-hidden="true">
|
|
253
|
+
{instructions}
|
|
254
|
+
</div>
|
|
253
255
|
)}
|
|
254
256
|
<ul className={className} style={{listStyle: "none"}}>
|
|
255
257
|
{choices.map(function (choice, i) {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
3
|
+
|
|
4
|
+
function getA11yText(
|
|
5
|
+
letter: string,
|
|
6
|
+
checked: boolean,
|
|
7
|
+
correct: boolean,
|
|
8
|
+
crossedOut: boolean,
|
|
9
|
+
showCorrectness: boolean,
|
|
10
|
+
): string {
|
|
11
|
+
// There are two pieces of metadata we want to add to each a11yText:
|
|
12
|
+
// whether the answer was checked/crossed-out/neither, and whether the
|
|
13
|
+
// answer is correct/incorrect/not-yet-revealed.
|
|
14
|
+
//
|
|
15
|
+
// Translation is tricky for cross-product situations like this, so
|
|
16
|
+
// we've just enumerated all 9 possibilities as separate strings.
|
|
17
|
+
if (showCorrectness && correct) {
|
|
18
|
+
if (checked) {
|
|
19
|
+
return i18n._("(Choice %(letter)s, Checked, Correct)", {
|
|
20
|
+
letter,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (crossedOut) {
|
|
24
|
+
return i18n._("(Choice %(letter)s, Crossed out, Correct)", {
|
|
25
|
+
letter,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
return i18n._("(Choice %(letter)s, Correct)", {
|
|
29
|
+
letter,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (showCorrectness && !correct) {
|
|
33
|
+
if (checked) {
|
|
34
|
+
return i18n._("(Choice %(letter)s, Checked, Incorrect)", {
|
|
35
|
+
letter,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (crossedOut) {
|
|
39
|
+
return i18n._("(Choice %(letter)s, Crossed out, Incorrect)", {
|
|
40
|
+
letter,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return i18n._("(Choice %(letter)s, Incorrect)", {
|
|
44
|
+
letter,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (checked) {
|
|
48
|
+
return i18n._("(Choice %(letter)s, Checked)", {
|
|
49
|
+
letter,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (crossedOut) {
|
|
53
|
+
return i18n._("(Choice %(letter)s, Crossed out)", {
|
|
54
|
+
letter,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return i18n._("(Choice %(letter)s)", {
|
|
58
|
+
letter,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export default getA11yText;
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
/* Component for rendering a letter icon in radio choice */
|
|
3
3
|
|
|
4
4
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
5
|
-
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
6
5
|
import * as React from "react";
|
|
7
6
|
|
|
8
7
|
import {getChoiceLetter} from "../util.js";
|
|
@@ -34,64 +33,6 @@ type ChoiceIconProps = {|
|
|
|
34
33
|
primaryProductColor?: string,
|
|
35
34
|
|};
|
|
36
35
|
|
|
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
36
|
function ChoiceIcon(props: ChoiceIconProps): React.Node {
|
|
96
37
|
const {
|
|
97
38
|
pos,
|
|
@@ -111,19 +52,11 @@ function ChoiceIcon(props: ChoiceIconProps): React.Node {
|
|
|
111
52
|
} = props;
|
|
112
53
|
|
|
113
54
|
const letter = getChoiceLetter(pos);
|
|
114
|
-
const a11yText = getA11yText(
|
|
115
|
-
letter,
|
|
116
|
-
checked,
|
|
117
|
-
correct,
|
|
118
|
-
crossedOut,
|
|
119
|
-
showCorrectness,
|
|
120
|
-
);
|
|
121
55
|
|
|
122
56
|
if (product === "sat") {
|
|
123
57
|
return (
|
|
124
58
|
<SATChoiceIcon
|
|
125
59
|
letter={letter}
|
|
126
|
-
a11yText={a11yText}
|
|
127
60
|
reviewMode={reviewMode}
|
|
128
61
|
checked={checked}
|
|
129
62
|
correct={correct}
|
|
@@ -135,7 +68,6 @@ function ChoiceIcon(props: ChoiceIconProps): React.Node {
|
|
|
135
68
|
return (
|
|
136
69
|
<LibraryChoiceIcon
|
|
137
70
|
letter={letter}
|
|
138
|
-
a11yText={a11yText}
|
|
139
71
|
reviewMode={reviewMode}
|
|
140
72
|
checked={checked}
|
|
141
73
|
crossedOut={crossedOut}
|
|
@@ -15,7 +15,6 @@ import sharedStyles, {LIBRARY_ICON_SIZE} from "./shared-styles.js";
|
|
|
15
15
|
|
|
16
16
|
type LibraryChoiceIconProps = {
|
|
17
17
|
letter: string,
|
|
18
|
-
a11yText: string,
|
|
19
18
|
checked: boolean,
|
|
20
19
|
crossedOut: boolean,
|
|
21
20
|
focused: boolean,
|
|
@@ -107,7 +106,6 @@ function getDynamicStyles(
|
|
|
107
106
|
|
|
108
107
|
function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
|
|
109
108
|
const {
|
|
110
|
-
a11yText,
|
|
111
109
|
checked,
|
|
112
110
|
crossedOut,
|
|
113
111
|
showCorrectness,
|
|
@@ -155,14 +153,11 @@ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
|
|
|
155
153
|
// radio icon
|
|
156
154
|
data-is-radio-icon={true}
|
|
157
155
|
>
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
correct={correct}
|
|
164
|
-
/>
|
|
165
|
-
</div>
|
|
156
|
+
<ChoiceInner
|
|
157
|
+
letter={letter}
|
|
158
|
+
showCorrectness={showCorrectness}
|
|
159
|
+
correct={correct}
|
|
160
|
+
/>
|
|
166
161
|
</div>
|
|
167
162
|
</FocusRing>
|
|
168
163
|
{crossedOut && <CrossOutLine color={dynamicStyles.borderColor} />}
|
|
@@ -11,7 +11,6 @@ import sharedStyles, {SAT_ICON_SIZE} from "./shared-styles.js";
|
|
|
11
11
|
|
|
12
12
|
type SATChoiceIconProps = {
|
|
13
13
|
letter: string,
|
|
14
|
-
a11yText: string,
|
|
15
14
|
checked: boolean,
|
|
16
15
|
multipleSelect: boolean,
|
|
17
16
|
correct: boolean,
|
|
@@ -66,15 +65,8 @@ function constructSATStyles(
|
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
function SATChoiceIcon(props: SATChoiceIconProps): React.Node {
|
|
69
|
-
const {
|
|
70
|
-
|
|
71
|
-
a11yText,
|
|
72
|
-
crossedOut,
|
|
73
|
-
reviewMode,
|
|
74
|
-
correct,
|
|
75
|
-
checked,
|
|
76
|
-
multipleSelect,
|
|
77
|
-
} = props;
|
|
68
|
+
const {letter, crossedOut, reviewMode, correct, checked, multipleSelect} =
|
|
69
|
+
props;
|
|
78
70
|
|
|
79
71
|
const {color, backgroundColor, borderColor, borderRadius} =
|
|
80
72
|
constructSATStyles(reviewMode, correct, checked, multipleSelect);
|
|
@@ -87,8 +79,7 @@ function SATChoiceIcon(props: SATChoiceIconProps): React.Node {
|
|
|
87
79
|
style={{backgroundColor, borderColor, borderRadius}}
|
|
88
80
|
/>
|
|
89
81
|
<div style={{color}} className={css(styles.letter)}>
|
|
90
|
-
<span
|
|
91
|
-
<span aria-hidden="true">{letter}</span>
|
|
82
|
+
<span>{letter}</span>
|
|
92
83
|
</div>
|
|
93
84
|
{crossedOut && <CrossOutLine color={borderColor} sat={true} />}
|
|
94
85
|
</div>
|