@khanacademy/perseus 1.2.0 → 1.3.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 (29) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/es/index.css +3 -3
  3. package/dist/es/index.js +145 -179
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +3 -3
  6. package/dist/index.js +143 -177
  7. package/dist/index.js.map +1 -1
  8. package/package.json +2 -2
  9. package/src/index.js +1 -0
  10. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +21 -11
  11. package/src/styles/widgets/passage.less +7 -3
  12. package/src/widgets/__stories__/passage-ref.stories.jsx +6 -2
  13. package/src/widgets/__stories__/passage.stories.jsx +9 -1
  14. package/src/widgets/__stories__/radio.stories.jsx +11 -0
  15. package/src/widgets/__testdata__/passage-ref_testdata.js +62 -0
  16. package/src/widgets/__testdata__/passage_testdata.js +25 -0
  17. package/src/widgets/__testdata__/radio_testdata.js +52 -0
  18. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +21 -11
  19. package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +18 -18
  20. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +36 -36
  21. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +221 -117
  22. package/src/widgets/__tests__/passage_test.jsx +6 -8
  23. package/src/widgets/__tests__/radio_test.jsx +3 -1
  24. package/src/widgets/passage.jsx +12 -41
  25. package/src/widgets/radio/__stories__/sat-styles.stories.jsx +191 -0
  26. package/src/widgets/radio/base-radio.jsx +1 -2
  27. package/src/widgets/radio/choice.jsx +4 -10
  28. package/src/widgets/radio/widget.jsx +128 -129
  29. package/src/widgets/passage/get-line-height-for-node.js +0 -9
@@ -8,15 +8,12 @@ import {testDependencies} from "../../../../../testing/test-dependencies.js";
8
8
  import * as Dependencies from "../../dependencies.js";
9
9
  import {ApiOptions} from "../../perseus-api.jsx";
10
10
  import {question1, question2} from "../__testdata__/passage_testdata.js";
11
- import PassageWidgetExport from "../passage.jsx";
12
- import * as getLineHeightModule from "../passage/get-line-height-for-node.js";
11
+ import PassageWidgetExport, {LineHeightMeasurer} from "../passage.jsx";
13
12
 
14
13
  import {renderQuestion} from "./renderQuestion.jsx";
15
14
 
16
15
  import type {APIOptions} from "../../types.js";
17
16
 
18
- jest.mock("../passage/get-line-height-for-node.js");
19
-
20
17
  function renderPassage(overwrite) {
21
18
  const widgetPropsBase = {
22
19
  footnotes: "",
@@ -53,10 +50,6 @@ function renderPassage(overwrite) {
53
50
 
54
51
  describe("passage widget", () => {
55
52
  beforeEach(() => {
56
- jest.spyOn(getLineHeightModule, "getLineHeightForNode").mockReturnValue(
57
- 21,
58
- );
59
-
60
53
  // passage widget uses the height of the entire rendered passage along
61
54
  // with the above line height calculations to determine the number of
62
55
  // lines rendered (as a way to provide line numbering).
@@ -66,6 +59,11 @@ describe("passage widget", () => {
66
59
  "get",
67
60
  ).mockReturnValue(200.0);
68
61
 
62
+ jest.spyOn(
63
+ LineHeightMeasurer.prototype,
64
+ "measureLineHeight",
65
+ ).mockReturnValue(20);
66
+
69
67
  jest.spyOn(Dependencies, "getDependencies").mockReturnValue(
70
68
  testDependencies,
71
69
  );
@@ -800,7 +800,9 @@ describe("multi-choice question", () => {
800
800
 
801
801
  // Act
802
802
  const option = screen.getAllByRole("checkbox");
803
- userEvent.click(option[3]); // incorrect
803
+ userEvent.click(option[0]); // correct
804
+ userEvent.click(option[1]); // correct
805
+ userEvent.click(option[2]); // incorrect
804
806
 
805
807
  // Assert
806
808
  expect(renderer).toHaveInvalidInput(
@@ -1,5 +1,4 @@
1
1
  // @flow
2
-
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
5
4
  import {StyleSheet, css} from "aphrodite";
@@ -12,7 +11,6 @@ import HighlightableContent from "../components/highlighting/highlightable-conte
12
11
  import {getDependencies} from "../dependencies.js";
13
12
  import Renderer from "../renderer.jsx";
14
13
 
15
- import {getLineHeightForNode} from "./passage/get-line-height-for-node.js";
16
14
  import PassageMarkdown from "./passage/passage-markdown.jsx";
17
15
 
18
16
  import type {SerializedHighlightSet} from "../components/highlighting/types.js";
@@ -27,51 +25,24 @@ import type {
27
25
  import type {ParseState} from "./passage/passage-markdown.jsx";
28
26
  import type {SingleASTNode} from "@khanacademy/simple-markdown";
29
27
 
30
- // A fake paragraph to measure the line height of the passage. In CSS we always
31
- // set the line height to 22 pixels, but when using the browser zoom feature,
32
- // the line height often ends up being a fractional number of pixels close to
33
- // 22 pixels.
34
- class LineHeightMeasurer extends React.Component<{...}> {
35
- _cachedLineHeight: number;
36
- body: ?HTMLDivElement;
37
- end: ?HTMLDivElement;
28
+ // A fake paragraph to measure the line height of the passage,
29
+ // so we can adapt to browser zoom
30
+ export class LineHeightMeasurer extends React.Component<{...}> {
31
+ _line: ?HTMLDivElement;
38
32
 
39
33
  measureLineHeight(): number {
40
- if (typeof this._cachedLineHeight !== "number") {
41
- this.forceMeasureLineHeight();
42
- }
43
-
44
- return this._cachedLineHeight;
45
- }
46
-
47
- forceMeasureLineHeight() {
48
- const body = this.body;
49
- const end = this.end;
50
-
51
- if (!body || !end) {
52
- return;
34
+ if (!this._line) {
35
+ return 0;
53
36
  }
54
37
 
55
- // Add some text which magically fills an entire line.
56
- body.textContent = " \u0080";
57
-
58
- // Now, the line height is the difference between the top of the
59
- // second line and the top of the first line.
60
- this._cachedLineHeight = getLineHeightForNode(body, end);
61
-
62
- // Clear out the first line so it doesn't overlap the passage.
63
- body.textContent = "";
38
+ return this._line.clientHeight;
64
39
  }
65
40
 
66
41
  render(): React.Node {
67
42
  return (
68
- <div className={css(styles.measurer)}>
69
- <div>
70
- <div
71
- ref={(ref) => (this.body = ref)}
72
- className="paragraph"
73
- />
74
- <div ref={(ref) => (this.end = ref)} />
43
+ <div className={css(styles.measurer)} aria-hidden="true">
44
+ <div ref={(ref) => (this._line = ref)} className="paragraph">
45
+ Line Height Measurement
75
46
  </div>
76
47
  </div>
77
48
  );
@@ -81,9 +52,9 @@ class LineHeightMeasurer extends React.Component<{...}> {
81
52
  const styles = StyleSheet.create({
82
53
  measurer: {
83
54
  position: "absolute",
84
- width: "100%",
85
55
  top: 0,
86
56
  left: 0,
57
+ visibility: "hidden",
87
58
  // keep from blocking text selection
88
59
  zIndex: -1,
89
60
  },
@@ -168,7 +139,7 @@ class Passage extends React.Component<PassageProps, PassageState> {
168
139
  // height changes we expect are subpixel changes when the user
169
140
  // zooms in/out, and the only way to listen for zoom events is to
170
141
  // listen for resize events.
171
- this._lineHeightMeasurerRef?.forceMeasureLineHeight();
142
+ this._lineHeightMeasurerRef?.measureLineHeight();
172
143
  this._updateState();
173
144
  }, 500);
174
145
  window.addEventListener("resize", this._onResize);
@@ -0,0 +1,191 @@
1
+ // @flow
2
+ import {action} from "@storybook/addon-actions";
3
+ import * as React from "react";
4
+
5
+ import {generateChoice} from "../../__testdata__/base-radio_testdata.js";
6
+ import BaseRadio from "../base-radio.jsx";
7
+
8
+ type StoryArgs = {|
9
+ multipleSelect: boolean,
10
+ editMode: boolean,
11
+ countChoices: boolean,
12
+ |};
13
+
14
+ type Story = {|
15
+ title: string,
16
+ args: StoryArgs,
17
+ |};
18
+
19
+ export default ({
20
+ title: "Perseus/Widgets/Radio/SAT Base Radio",
21
+ args: {
22
+ multipleSelect: false,
23
+ editMode: false,
24
+ countChoices: false,
25
+ },
26
+ }: Story);
27
+
28
+ const defaultProps = {
29
+ apiOptions: {
30
+ satStyling: true,
31
+ },
32
+ choices: [
33
+ generateChoice({
34
+ content: "Content 1",
35
+ }),
36
+ generateChoice({
37
+ content: "Content 2",
38
+ }),
39
+ generateChoice({
40
+ content: "Content 3",
41
+ correct: true,
42
+ }),
43
+ generateChoice({
44
+ isNoneOfTheAbove: true,
45
+ }),
46
+ ],
47
+ deselectEnabled: false,
48
+ editMode: false,
49
+ labelWrap: false,
50
+ countChoices: false,
51
+ numCorrect: 1,
52
+ multipleSelect: false,
53
+
54
+ // A callback indicating that this choice has changed. Its argument is
55
+ // an object with two keys: `checked` and `crossedOut`. Each contains
56
+ // an array of boolean values, specifying the new checked and
57
+ // crossed-out value of each choice.
58
+ onChange: action("changed"),
59
+
60
+ // Whether this widget was the most recently used widget in this
61
+ // Renderer. Determines whether we'll auto-scroll the page upon
62
+ // entering review mode.
63
+ isLastUsedWidget: false,
64
+ };
65
+
66
+ export const Interactive = (args: StoryArgs): React.Node => {
67
+ const overwrittenProps = {...defaultProps, ...args};
68
+ return <BaseRadio {...overwrittenProps} />;
69
+ };
70
+
71
+ export const SingleSelectWithNothingSelected = (
72
+ args: StoryArgs,
73
+ ): React.Node => {
74
+ const overwrittenProps = {...defaultProps, multipleSelect: false};
75
+ return <BaseRadio {...overwrittenProps} />;
76
+ };
77
+
78
+ export const MultipleSelectWithNothingSelected = (
79
+ args: StoryArgs,
80
+ ): React.Node => {
81
+ const overwrittenProps = {...defaultProps, multipleSelect: true};
82
+ return <BaseRadio {...overwrittenProps} />;
83
+ };
84
+
85
+ export const MultipleSelectWithCountChoicesLabel = (
86
+ args: StoryArgs,
87
+ ): React.Node => {
88
+ const overwrittenProps = {
89
+ ...defaultProps,
90
+ multipleSelect: true,
91
+ numCorrect: 2,
92
+ countChoices: true,
93
+ };
94
+ return <BaseRadio {...overwrittenProps} />;
95
+ };
96
+
97
+ export const SingleSelected = (args: StoryArgs): React.Node => {
98
+ const choices = Array(4)
99
+ .fill(null)
100
+ .map((_, i) => generateChoice({content: `Choice ${i + 1}`}));
101
+ choices[1].checked = true;
102
+
103
+ const overwrittenProps = {...defaultProps, multipleSelect: false, choices};
104
+ return <BaseRadio {...overwrittenProps} />;
105
+ };
106
+
107
+ export const MultipleSelected = (args: StoryArgs): React.Node => {
108
+ const choices = Array(4)
109
+ .fill(null)
110
+ .map((_, i) => generateChoice({content: `Choice ${i + 1}`}));
111
+ choices[1].checked = true;
112
+ choices[2].checked = true;
113
+
114
+ const overwrittenProps = {...defaultProps, multipleSelect: true, choices};
115
+ return <BaseRadio {...overwrittenProps} />;
116
+ };
117
+
118
+ export const SingleKitchenSink = (args: StoryArgs): React.Node => {
119
+ const choices = Array(4)
120
+ .fill(null)
121
+ .map((_, i) => {
122
+ const choice = generateChoice({
123
+ content: `Choice ${i + 1}`,
124
+ rationale: "This is a neat rationale",
125
+ hasRationale: true,
126
+ showRationale: true,
127
+ correct: false,
128
+ showCorrectness: true,
129
+ });
130
+
131
+ return choice;
132
+ });
133
+ choices[1].checked = true;
134
+ choices[2].correct = true;
135
+
136
+ const rubricChoices = choices.map(({correct}) => ({
137
+ // note(matthew): reviewModeRubric.choices requires content,
138
+ // but I don't see how it's getting used and Flow gets mad
139
+ // when I use choice.content because it's not a string.
140
+ // reviewModeRubric could probably use a look over.
141
+ content: "",
142
+ correct,
143
+ }));
144
+
145
+ const overwrittenProps = {
146
+ ...defaultProps,
147
+ multipleSelect: false,
148
+ reviewModeRubric: {choices: rubricChoices},
149
+ choices,
150
+ };
151
+ return <BaseRadio {...overwrittenProps} />;
152
+ };
153
+
154
+ export const MultipleKitchenSink = (args: StoryArgs): React.Node => {
155
+ const choices = Array(4)
156
+ .fill(null)
157
+ .map((_, i) => {
158
+ const choice = generateChoice({
159
+ content: `Choice ${i + 1}`,
160
+ rationale: "This is a neat rationale",
161
+ hasRationale: true,
162
+ showRationale: true,
163
+ correct: false,
164
+ showCorrectness: true,
165
+ });
166
+
167
+ return choice;
168
+ });
169
+ choices[1].checked = true;
170
+ choices[2].checked = true;
171
+ choices[2].correct = true;
172
+ choices[3].correct = true;
173
+
174
+ const rubricChoices = choices.map((c) => ({
175
+ // note(matthew): reviewModeRubric.choices requires content,
176
+ // but I don't see how it's getting used and Flow gets mad
177
+ // when I use choice.content because it's not a string.
178
+ // reviewModeRubric could probably use a look over.
179
+ content: "",
180
+ correct: c.correct,
181
+ }));
182
+
183
+ const overwrittenProps = {
184
+ ...defaultProps,
185
+ multipleSelect: true,
186
+ numCorrect: 2,
187
+ reviewModeRubric: {choices: rubricChoices},
188
+ choices,
189
+ };
190
+ return <BaseRadio {...overwrittenProps} />;
191
+ };
@@ -454,8 +454,7 @@ const styles: StyleDeclaration = StyleSheet.create({
454
454
  },
455
455
 
456
456
  radio: {
457
- // Avoid centering
458
- width: "100%",
457
+ padding: 0,
459
458
  },
460
459
 
461
460
  responsiveRadioContainer: {
@@ -14,7 +14,7 @@ import {useState, useEffect} from "react";
14
14
  import _ from "underscore";
15
15
 
16
16
  import Icon from "../../components/icon.jsx";
17
- import {ApiOptions} from "../../perseus-api.jsx";
17
+ import {ApiOptions, ClassNames} from "../../perseus-api.jsx";
18
18
  import * as styleConstants from "../../styles/constants.js";
19
19
  import mediaQueries from "../../styles/media-queries.js";
20
20
 
@@ -140,7 +140,6 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
140
140
  css(
141
141
  !apiOptions.satStyling && styles.description,
142
142
  apiOptions.satStyling && styles.satDescription,
143
- satCorrectChoice && styles.satDescriptionCorrect,
144
143
  satCorrectChoice && checked && styles.satDescriptionCorrectChecked,
145
144
  satIncorrectChecked && styles.satDescriptionIncorrectChecked,
146
145
  ),
@@ -179,12 +178,12 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
179
178
  dispay: "flex",
180
179
  flexDirection: "column",
181
180
  }}
181
+ className={descriptionClassName}
182
182
  >
183
183
  <div
184
184
  style={{
185
185
  display: "flex",
186
186
  flexDirection: "row",
187
- width: "100%",
188
187
  opacity: showDimmed ? 0.5 : 1.0,
189
188
  }}
190
189
  >
@@ -208,6 +207,7 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
208
207
  apiOptions.readOnly
209
208
  }
210
209
  tabIndex={-1}
210
+ className={ClassNames.RADIO.OPTION_CONTENT}
211
211
  />
212
212
  <label htmlFor={choiceId}>
213
213
  {a11yText} &nbsp; {content}
@@ -222,7 +222,6 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
222
222
  crossedOut: false,
223
223
  });
224
224
  }}
225
- className={descriptionClassName}
226
225
  disabled={
227
226
  disabled ||
228
227
  apiOptions.staticRender ||
@@ -390,11 +389,7 @@ const styles = StyleSheet.create({
390
389
  boxSizing: "border-box",
391
390
  cursor: "pointer",
392
391
  marginLeft: 0,
393
- padding: "17px 14px",
394
- },
395
-
396
- satDescriptionCorrect: {
397
- color: styleConstants.satCorrectColor,
392
+ padding: "17px 14px 17px 0",
398
393
  },
399
394
 
400
395
  satDescriptionCorrectChecked: {
@@ -402,7 +397,6 @@ const styles = StyleSheet.create({
402
397
  },
403
398
 
404
399
  satDescriptionIncorrectChecked: {
405
- color: styleConstants.satIncorrectColor,
406
400
  backgroundColor: styleConstants.satIncorrectBackgroundColor,
407
401
  },
408
402
 
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/sort-comp */
2
1
  // @flow
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import * as i18n from "@khanacademy/wonder-blocks-i18n";
@@ -66,6 +65,134 @@ class Radio extends React.Component<Props> {
66
65
  linterContext: linterContextDefault,
67
66
  };
68
67
 
68
+ static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
69
+ const numSelected = userInput.choicesSelected.reduce(
70
+ (sum, selected) => {
71
+ return sum + (selected ? 1 : 0);
72
+ },
73
+ 0,
74
+ );
75
+
76
+ if (numSelected === 0) {
77
+ return {
78
+ type: "invalid",
79
+ message: null,
80
+ };
81
+ }
82
+
83
+ if (
84
+ userInput.numCorrect &&
85
+ userInput.numCorrect > 1 &&
86
+ numSelected !== userInput.numCorrect
87
+ ) {
88
+ return {
89
+ type: "invalid",
90
+ message: i18n._("Please choose the correct number of answers."),
91
+ };
92
+ // If NOTA and some other answer are checked, ...
93
+ }
94
+ if (userInput.noneOfTheAboveSelected && numSelected > 1) {
95
+ return {
96
+ type: "invalid",
97
+ message: i18n._(
98
+ "'None of the above' may not be selected " +
99
+ "when other answers are selected.",
100
+ ),
101
+ };
102
+ }
103
+
104
+ const correct = userInput.choicesSelected.every((selected, i) => {
105
+ let isCorrect;
106
+ if (userInput.noneOfTheAboveIndex === i) {
107
+ isCorrect = rubric.choices.every((choice, j) => {
108
+ return i === j || !choice.correct;
109
+ });
110
+ } else {
111
+ isCorrect = !!rubric.choices[i].correct;
112
+ }
113
+ return isCorrect === selected;
114
+ });
115
+
116
+ return {
117
+ type: "points",
118
+ earned: correct ? 1 : 0,
119
+ total: 1,
120
+ message: null,
121
+ };
122
+ }
123
+
124
+ static getUserInputFromProps(props: Props): UserInput {
125
+ // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
126
+ // future timeline implementers: this used to be {value: i} before
127
+ // multiple select was added)
128
+ if (props.choiceStates) {
129
+ let noneOfTheAboveIndex = null;
130
+ let noneOfTheAboveSelected = false;
131
+
132
+ const choiceStates = props.choiceStates;
133
+ const choicesSelected = choiceStates.map(() => false);
134
+ const countChoices = props.countChoices;
135
+ const numCorrect = props.numCorrect;
136
+
137
+ for (let i = 0; i < choicesSelected.length; i++) {
138
+ const index = props.choices[i].originalIndex;
139
+
140
+ choicesSelected[index] = choiceStates[i].selected;
141
+
142
+ if (props.choices[i].isNoneOfTheAbove) {
143
+ noneOfTheAboveIndex = index;
144
+
145
+ if (choicesSelected[i]) {
146
+ noneOfTheAboveSelected = true;
147
+ }
148
+ }
149
+ }
150
+
151
+ return {
152
+ countChoices,
153
+ choicesSelected,
154
+ numCorrect,
155
+ noneOfTheAboveIndex,
156
+ noneOfTheAboveSelected,
157
+ };
158
+ // Support legacy choiceState implementation
159
+ }
160
+ /* istanbul ignore if - props.values is deprecated */
161
+ const {values} = props;
162
+ if (values) {
163
+ let noneOfTheAboveIndex = null;
164
+ let noneOfTheAboveSelected = false;
165
+
166
+ const choicesSelected = [...values];
167
+ const countChoices = props.countChoices;
168
+ const numCorrect = props.numCorrect;
169
+ const valuesLength = values.length;
170
+
171
+ for (let i = 0; i < valuesLength; i++) {
172
+ const index = props.choices[i].originalIndex;
173
+ choicesSelected[index] = values[i];
174
+
175
+ if (props.choices[i].isNoneOfTheAbove) {
176
+ noneOfTheAboveIndex = index;
177
+ if (choicesSelected[i]) {
178
+ noneOfTheAboveSelected = true;
179
+ }
180
+ }
181
+ }
182
+ return {
183
+ choicesSelected,
184
+ noneOfTheAboveIndex,
185
+ noneOfTheAboveSelected,
186
+ countChoices,
187
+ numCorrect,
188
+ };
189
+ }
190
+ // Nothing checked
191
+ return {
192
+ choicesSelected: props.choices.map(() => false),
193
+ };
194
+ }
195
+
69
196
  _renderRenderer: (content?: string) => React.Node = (
70
197
  content?: string = "",
71
198
  ) => {
@@ -382,134 +509,6 @@ class Radio extends React.Component<Props> {
382
509
  />
383
510
  );
384
511
  }
385
-
386
- static validate(userInput: UserInput, rubric: Rubric): PerseusScore {
387
- const numSelected = userInput.choicesSelected.reduce(
388
- (sum, selected) => {
389
- return sum + (selected ? 1 : 0);
390
- },
391
- 0,
392
- );
393
-
394
- if (numSelected === 0) {
395
- return {
396
- type: "invalid",
397
- message: null,
398
- };
399
- }
400
-
401
- if (
402
- userInput.numCorrect &&
403
- userInput.numCorrect > 1 &&
404
- numSelected !== userInput.numCorrect
405
- ) {
406
- return {
407
- type: "invalid",
408
- message: i18n._("Please choose the correct number of answers."),
409
- };
410
- // If NOTA and some other answer are checked, ...
411
- }
412
- if (userInput.noneOfTheAboveSelected && numSelected > 1) {
413
- return {
414
- type: "invalid",
415
- message: i18n._(
416
- "'None of the above' may not be selected " +
417
- "when other answers are selected.",
418
- ),
419
- };
420
- }
421
-
422
- const correct = userInput.choicesSelected.every((selected, i) => {
423
- let isCorrect;
424
- if (userInput.noneOfTheAboveIndex === i) {
425
- isCorrect = rubric.choices.every((choice, j) => {
426
- return i === j || !choice.correct;
427
- });
428
- } else {
429
- isCorrect = !!rubric.choices[i].correct;
430
- }
431
- return isCorrect === selected;
432
- });
433
-
434
- return {
435
- type: "points",
436
- earned: correct ? 1 : 0,
437
- total: 1,
438
- message: null,
439
- };
440
- }
441
-
442
- static getUserInputFromProps(props: Props): UserInput {
443
- // Return checked inputs in the form {choicesSelected: [bool]}. (Dear
444
- // future timeline implementers: this used to be {value: i} before
445
- // multiple select was added)
446
- if (props.choiceStates) {
447
- let noneOfTheAboveIndex = null;
448
- let noneOfTheAboveSelected = false;
449
-
450
- const choiceStates = props.choiceStates;
451
- const choicesSelected = choiceStates.map(() => false);
452
- const countChoices = props.countChoices;
453
- const numCorrect = props.numCorrect;
454
-
455
- for (let i = 0; i < choicesSelected.length; i++) {
456
- const index = props.choices[i].originalIndex;
457
-
458
- choicesSelected[index] = choiceStates[i].selected;
459
-
460
- if (props.choices[i].isNoneOfTheAbove) {
461
- noneOfTheAboveIndex = index;
462
-
463
- if (choicesSelected[i]) {
464
- noneOfTheAboveSelected = true;
465
- }
466
- }
467
- }
468
-
469
- return {
470
- countChoices,
471
- choicesSelected,
472
- numCorrect,
473
- noneOfTheAboveIndex,
474
- noneOfTheAboveSelected,
475
- };
476
- // Support legacy choiceState implementation
477
- }
478
- /* istanbul ignore if - props.values is deprecated */
479
- const {values} = props;
480
- if (values) {
481
- let noneOfTheAboveIndex = null;
482
- let noneOfTheAboveSelected = false;
483
-
484
- const choicesSelected = [...values];
485
- const countChoices = props.countChoices;
486
- const numCorrect = props.numCorrect;
487
- const valuesLength = values.length;
488
-
489
- for (let i = 0; i < valuesLength; i++) {
490
- const index = props.choices[i].originalIndex;
491
- choicesSelected[index] = values[i];
492
-
493
- if (props.choices[i].isNoneOfTheAbove) {
494
- noneOfTheAboveIndex = index;
495
- if (choicesSelected[i]) {
496
- noneOfTheAboveSelected = true;
497
- }
498
- }
499
- }
500
- return {
501
- choicesSelected,
502
- noneOfTheAboveIndex,
503
- noneOfTheAboveSelected,
504
- countChoices,
505
- numCorrect,
506
- };
507
- }
508
- // Nothing checked
509
- return {
510
- choicesSelected: props.choices.map(() => false),
511
- };
512
- }
513
512
  }
514
513
 
515
514
  export default Radio;
@@ -1,9 +0,0 @@
1
- // @flow
2
- import $ from "jquery";
3
-
4
- export const getLineHeightForNode = (
5
- line1: HTMLDivElement,
6
- line2: HTMLDivElement,
7
- ): number => {
8
- return $(line2).offset().top - $(line1).offset().top;
9
- };