@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.
Files changed (49) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/es/index.css +17 -8
  3. package/dist/es/index.js +258 -223
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +17 -8
  6. package/dist/index.js +261 -225
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/components/__stories__/input-with-examples.stories.jsx +1 -0
  10. package/src/components/input-with-examples.jsx +41 -24
  11. package/src/components/number-input.jsx +2 -2
  12. package/src/components/simple-keypad-input.jsx +18 -21
  13. package/src/components/text-input.jsx +5 -3
  14. package/src/index.js +1 -1
  15. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +356 -65
  16. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  17. package/src/perseus-types.js +18 -7
  18. package/src/styles/perseus-renderer.less +8 -2
  19. package/src/styles/widgets/matrix.less +2 -0
  20. package/src/styles/widgets/passage.less +9 -5
  21. package/src/util/answer-types.js +2 -3
  22. package/src/util/math.js +3 -10
  23. package/src/widgets/__stories__/numeric-input.stories.jsx +94 -7
  24. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  25. package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +15 -6
  26. package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +5 -2
  27. package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +4 -1
  28. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +228 -67
  29. package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +24 -24
  30. package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +4 -4
  31. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +2581 -679
  32. package/src/widgets/__tests__/group_test.jsx +4 -4
  33. package/src/widgets/__tests__/numeric-input_test.jsx +12 -8
  34. package/src/widgets/__tests__/passage_test.jsx +81 -0
  35. package/src/widgets/__tests__/radio/base-radio_test.jsx +55 -7
  36. package/src/widgets/__tests__/radio/choice_test.jsx +164 -63
  37. package/src/widgets/__tests__/radio_test.jsx +39 -15
  38. package/src/widgets/definition.jsx +1 -1
  39. package/src/widgets/graded-group.jsx +5 -1
  40. package/src/widgets/group.jsx +84 -102
  41. package/src/widgets/input-number.jsx +6 -1
  42. package/src/widgets/matrix.jsx +17 -1
  43. package/src/widgets/numeric-input.jsx +99 -86
  44. package/src/widgets/radio/base-radio.jsx +3 -1
  45. package/src/widgets/radio/choice-a11y-text.js +62 -0
  46. package/src/widgets/radio/choice-icon/choice-icon.jsx +0 -68
  47. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +5 -10
  48. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +3 -12
  49. 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 classNames from "classnames";
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
- PerseusNumericInputWidgetOptions,
19
+ MathFormat,
20
20
  PerseusNumericInputAnswer,
21
- } from "../perseus-types.js";
22
- import type {PerseusScore, WidgetExports, WidgetProps} from "../types.js";
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: 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 = _.map(correctAnswers, (answer) => {
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 = _.pluck(answerFormButtons, "value");
151
-
152
- const createValidator = (answer) =>
153
- KhanAnswerTypes.number.createValidatorFunctional(answer.value, {
154
- message: answer.message,
155
- simplify:
156
- answer.status === "correct" ? answer.simplify : "optional",
157
- inexact: true, // TODO(merlob) backfill / delete
158
- maxError: answer.maxError,
159
- forms:
160
- answer.strict &&
161
- answer.answerForms &&
162
- answer.answerForms.length !== 0
163
- ? answer.answerForms
164
- : allAnswerForms,
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 = _.where(rubric.answers, {status: "correct"});
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 = _.find(
181
- _.map(correctAnswers, (answer) => {
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
- _.where(rubric.answers, {status: "ungraded"}),
206
- _.where(rubric.answers, {status: "wrong"}),
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 = _.find(otherAnswers, (answer) => {
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 = _.filter(
263
- rubric.answers,
276
+ const correctAnswers = rubric.answers.filter(
264
277
  (answer) => answer.status === "correct",
265
278
  );
266
- const answerStrings = _.map(correctAnswers, (answer) => {
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
- : _.map(_.keys(formExamples), (name) => {
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 = answerFormNames.length >= _.size(formExamples);
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
- // eslint-disable-next-line react/no-string-refs
336
- this.refs.input.focus();
349
+ this.inputRef?.focus();
337
350
  return true;
338
351
  };
339
352
 
340
- focusInputPath: ($FlowFixMe) => void = (inputPath) => {
341
- /* istanbul ignore next */
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: ($FlowFixMe) => void = (inputPath) => {
347
- // eslint-disable-next-line react/no-string-refs
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: ($FlowFixMe) => string = (inputPath) => {
368
+ getGrammarTypeForPath: (FocusPath) => string = (inputPath) => {
359
369
  /* istanbul ignore next */
360
370
  return "number";
361
371
  };
362
372
 
363
- setInputValue: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => void = (
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, $FlowFixMe) => void = (newValue, cb) => {
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
- // eslint-disable-next-line react/no-string-refs
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
- // eslint-disable-next-line react/no-string-refs
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
- // $FlowFixMe[prop-missing]
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: ($FlowFixMe) => $FlowFixMe = function (
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 _.reduce(
508
- list,
509
- (uniqueList, element) => {
510
- // For each element, decide whether it's already in the list of
511
- // unique items.
512
- const inList = _.find(uniqueList, iteratee.bind(null, element));
513
- if (inList) {
514
- return uniqueList;
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 = _.flatten(answerFormsList);
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 _.keys(formExamples).indexOf(form.name);
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: string,
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 const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
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
- _.map(widgetOptions.answers, (answer) => {
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}>{instructions}</div>
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
- <div className="perseus-sr-only">{a11yText}</div>
159
- <div aria-hidden>
160
- <ChoiceInner
161
- letter={letter}
162
- showCorrectness={showCorrectness}
163
- correct={correct}
164
- />
165
- </div>
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
- letter,
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 className="perseus-sr-only">{a11yText}</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>