@khanacademy/perseus 0.4.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.eslintrc.js +6 -1
  2. package/CHANGELOG.md +45 -0
  3. package/dist/es/index.css +15 -8
  4. package/dist/es/index.js +1214 -1404
  5. package/dist/es/index.js.map +1 -1
  6. package/dist/index.css +15 -8
  7. package/dist/index.js +1217 -1411
  8. package/dist/index.js.map +1 -1
  9. package/dist/strings.js +0 -4
  10. package/package.json +9 -8
  11. package/src/components/number-input.jsx +2 -2
  12. package/src/components/simple-keypad-input.jsx +18 -21
  13. package/src/index.js +1 -1
  14. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +426 -135
  15. package/src/multi-items/__tests__/multi-renderer_test.js +9 -9
  16. package/src/perseus-api.jsx +10 -36
  17. package/src/perseus-types.js +18 -145
  18. package/src/styles/constants.js +0 -2
  19. package/src/styles/perseus-renderer.less +8 -2
  20. package/src/styles/variables.less +0 -2
  21. package/src/styles/widgets/passage.less +9 -5
  22. package/src/types.js +18 -35
  23. package/src/util/answer-types.js +2 -3
  24. package/src/util/math.js +3 -10
  25. package/src/util.js +0 -6
  26. package/src/version.js +1 -1
  27. package/src/widgets/__stories__/numeric-input.stories.jsx +43 -2
  28. package/src/widgets/__testdata__/base-radio_testdata.js +22 -0
  29. package/src/widgets/__tests__/__snapshots__/definition_test.js.snap +4 -4
  30. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +291 -130
  31. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +3293 -1415
  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 +291 -41
  36. package/src/widgets/__tests__/radio/choice-icon_test.jsx +67 -0
  37. package/src/widgets/__tests__/radio/choice_test.jsx +302 -0
  38. package/src/widgets/__tests__/radio/focus-ring_test.jsx +23 -0
  39. package/src/widgets/__tests__/radio/option-status_test.jsx +65 -0
  40. package/src/widgets/__tests__/radio_test.jsx +39 -15
  41. package/src/widgets/definition.jsx +1 -1
  42. package/src/widgets/group.jsx +84 -102
  43. package/src/widgets/input-number.jsx +6 -1
  44. package/src/widgets/numeric-input.jsx +87 -80
  45. package/src/widgets/passage/__tests__/passage-markdown_test.jsx +0 -9
  46. package/src/widgets/passage/get-line-height-for-node.js +4 -5
  47. package/src/widgets/passage/passage-markdown.jsx +176 -120
  48. package/src/widgets/passage.jsx +48 -46
  49. package/src/widgets/radio/__stories__/base-radio.stories.jsx +164 -17
  50. package/src/widgets/radio/__stories__/choice-icon.stories.jsx +202 -111
  51. package/src/widgets/radio/__stories__/choice-none-above.stories.jsx +21 -6
  52. package/src/widgets/radio/__stories__/choice.stories.jsx +138 -0
  53. package/src/widgets/radio/__stories__/focus-ring.stories.jsx +29 -23
  54. package/src/widgets/radio/__stories__/option-status.stories.jsx +93 -99
  55. package/src/widgets/radio/base-radio.jsx +485 -556
  56. package/src/widgets/radio/choice-a11y-text.js +62 -0
  57. package/src/widgets/radio/choice-icon/choice-icon.jsx +87 -0
  58. package/src/widgets/radio/choice-icon/cross-out-line.jsx +55 -0
  59. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +214 -0
  60. package/src/widgets/radio/choice-icon/sat-choice-icon.jsx +120 -0
  61. package/src/widgets/radio/choice-icon/shared-styles.js +17 -0
  62. package/src/widgets/radio/choice-none-above.jsx +49 -40
  63. package/src/widgets/radio/choice.jsx +319 -347
  64. package/src/widgets/radio/choice.jsx.fixture.js +0 -18
  65. package/src/widgets/radio/focus-ring.jsx +29 -22
  66. package/src/widgets/radio/option-status.jsx +58 -36
  67. package/src/widgets/radio/widget.jsx +181 -136
  68. package/src/widgets/radio.jsx +16 -17
  69. package/src/widgets/radio/__stories__/cross-out-button.stories.jsx +0 -64
  70. package/src/widgets/radio/choice-icon.jsx +0 -469
  71. package/src/widgets/radio/cross-out-button.jsx +0 -134
@@ -1,16 +1,17 @@
1
- /* eslint-disable react/sort-comp */
2
1
  // @flow
3
2
  import {linterContextDefault} from "@khanacademy/perseus-linter";
4
3
  import classNames from "classnames";
5
4
  import * as React from "react";
6
- import _ from "underscore";
7
5
 
8
6
  import * as Changeable from "../mixins/changeable.jsx";
9
7
  import {ApiOptions} from "../perseus-api.jsx";
10
8
  import Renderer from "../renderer.jsx";
11
9
 
12
10
  import type {PerseusGroupWidgetOptions} from "../perseus-types.js";
11
+ import type {Widget} from "../renderer.jsx";
13
12
  import type {
13
+ APIOptions,
14
+ ChangeFn,
14
15
  FocusPath,
15
16
  PerseusScore,
16
17
  WidgetExports,
@@ -28,6 +29,8 @@ type DefaultProps = {|
28
29
  |};
29
30
 
30
31
  class Group extends React.Component<Props> {
32
+ rendererRef: ?Renderer;
33
+
31
34
  static defaultProps: DefaultProps = {
32
35
  content: "",
33
36
  widgets: {},
@@ -42,24 +45,76 @@ class Group extends React.Component<Props> {
42
45
  this.forceUpdate();
43
46
  }
44
47
 
48
+ change: ChangeFn = (...args) => {
49
+ return Changeable.change.apply(this, args);
50
+ };
51
+
52
+ getUserInput: () => $FlowFixMe = () => {
53
+ return this.rendererRef?.getUserInput();
54
+ };
55
+
56
+ getSerializedState: () => $FlowFixMe = () => {
57
+ return this.rendererRef?.getSerializedState();
58
+ };
59
+
60
+ restoreSerializedState: ($FlowFixMe, $FlowFixMe) => null = (
61
+ state,
62
+ callback,
63
+ ) => {
64
+ this.rendererRef?.restoreSerializedState(state, callback);
65
+ // Tell our renderer that we have no props to change
66
+ // (all our changes were in state):
67
+ return null;
68
+ };
69
+
70
+ simpleValidate: (Rubric) => ?PerseusScore = (rubric) => {
71
+ return this.rendererRef?.score();
72
+ };
73
+
74
+ // Mobile API:
75
+ getInputPaths: () => ?$ReadOnlyArray<FocusPath> = () => {
76
+ return this.rendererRef?.getInputPaths();
77
+ };
78
+
79
+ setInputValue: (FocusPath, string, () => mixed) => void = (
80
+ path,
81
+ newValue,
82
+ callback,
83
+ ) => {
84
+ return this.rendererRef?.setInputValue(path, newValue, callback);
85
+ };
86
+
87
+ focus: () => ?boolean = () => {
88
+ return this.rendererRef?.focus();
89
+ };
90
+
91
+ focusInputPath: (FocusPath) => void = (path) => {
92
+ this.rendererRef?.focusPath(path);
93
+ };
94
+
95
+ blurInputPath: (FocusPath) => void = (path) => {
96
+ this.rendererRef?.blurPath(path);
97
+ };
98
+
99
+ showRationalesForCurrentlySelectedChoices: () => void = () => {
100
+ this.rendererRef?.showRationalesForCurrentlySelectedChoices();
101
+ };
102
+
45
103
  render(): React.Node {
46
- const apiOptions = _.extend(
47
- {},
48
- ApiOptions.defaults,
49
- this.props.apiOptions,
50
- {
51
- // Api Rewriting to support correct onFocus/onBlur
52
- // events for the mobile API
53
- onFocusChange: (newFocus, oldFocus) => {
54
- if (oldFocus) {
55
- this.props.onBlur(oldFocus);
56
- }
57
- if (newFocus) {
58
- this.props.onFocus(newFocus);
59
- }
60
- },
104
+ const apiOptions: APIOptions = {
105
+ ...ApiOptions.defaults,
106
+ ...this.props.apiOptions,
107
+ // Api Rewriting to support correct onFocus/onBlur
108
+ // events for the mobile API
109
+ onFocusChange: (newFocus, oldFocus) => {
110
+ if (oldFocus) {
111
+ this.props.onBlur(oldFocus);
112
+ }
113
+ if (newFocus) {
114
+ this.props.onFocus(newFocus);
115
+ }
61
116
  },
62
- );
117
+ };
63
118
 
64
119
  // Allow a problem number annotation to be added.
65
120
  // This is cyclical and should probably be reconsidered. In order to
@@ -69,7 +124,9 @@ class Group extends React.Component<Props> {
69
124
  // give ourselves number -1. To combat this, we forceUpdate in
70
125
  // componentDidMount so that we can number ourselves properly. But,
71
126
  // really we should have a more unidirectional flow. TODO(marcia): fix.
72
- const number = _.indexOf(this.props.findWidgets("group"), this);
127
+ const groupWidgets: $ReadOnlyArray<Widget> =
128
+ this.props.findWidgets("group");
129
+ const number: number = groupWidgets.indexOf(this);
73
130
  const problemNumComponent = this.props.apiOptions.groupAnnotator(
74
131
  number,
75
132
  this.props.widgetId,
@@ -81,15 +138,12 @@ class Group extends React.Component<Props> {
81
138
  // alert our renderer (our parent) of the fact that some interaction
82
139
  // has occurred.
83
140
  const onInteractWithWidget = (id) => {
84
- // eslint-disable-next-line react/no-string-refs
85
- if (this.refs.renderer) {
86
- // eslint-disable-next-line react/no-string-refs
87
- this.change("widgets", this.refs.renderer.props.widgets);
141
+ if (this.rendererRef) {
142
+ this.change("widgets", this.rendererRef.props.widgets);
88
143
  }
89
144
  };
90
145
 
91
- // eslint-disable-next-line react/no-string-refs
92
- const score = this.refs.renderer && this.refs.renderer.score();
146
+ const score = this.rendererRef?.score();
93
147
  const isValid = score && score.type !== "invalid";
94
148
  const isInvalid = score && score.type === "invalid";
95
149
 
@@ -104,18 +158,11 @@ class Group extends React.Component<Props> {
104
158
  })}
105
159
  >
106
160
  {problemNumComponent}
107
- {/**
108
- * We're passing a bunch of extra props to Renderer here that it
109
- * doesn't need. We should replace {...this.props} with the individual
110
- * props that are needed.
111
- * TODO(FEI-4034): Only pass what the Renderer expects.
112
- */}
113
- {/* $FlowFixMe[prop-missing] */}
114
- {/* $FlowFixMe[incompatible-type] */}
115
161
  <Renderer
116
- {...this.props}
117
- // eslint-disable-next-line react/no-string-refs
118
- ref="renderer"
162
+ content={this.props.content}
163
+ widgets={this.props.widgets}
164
+ images={this.props.images}
165
+ ref={(ref) => (this.rendererRef = ref)}
119
166
  apiOptions={apiOptions}
120
167
  findExternalWidgets={this.props.findWidgets}
121
168
  reviewMode={!!this.props.reviewModeRubric}
@@ -129,78 +176,13 @@ class Group extends React.Component<Props> {
129
176
  </div>
130
177
  );
131
178
  }
132
-
133
- change: ($FlowFixMe, $FlowFixMe, $FlowFixMe) => $FlowFixMe = (...args) => {
134
- return Changeable.change.apply(this, args);
135
- };
136
-
137
- getUserInput: () => $FlowFixMe = () => {
138
- // eslint-disable-next-line react/no-string-refs
139
- return this.refs.renderer.getUserInput();
140
- };
141
-
142
- getSerializedState: () => $FlowFixMe = () => {
143
- // eslint-disable-next-line react/no-string-refs
144
- return this.refs.renderer.getSerializedState();
145
- };
146
-
147
- restoreSerializedState: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
148
- state,
149
- callback,
150
- ) => {
151
- // eslint-disable-next-line react/no-string-refs
152
- this.refs.renderer.restoreSerializedState(state, callback);
153
- // Tell our renderer that we have no props to change
154
- // (all our changes were in state):
155
- return null;
156
- };
157
-
158
- simpleValidate: (Rubric) => PerseusScore = (rubric) => {
159
- // eslint-disable-next-line react/no-string-refs
160
- return this.refs.renderer.score();
161
- };
162
-
163
- // Mobile API:
164
- getInputPaths: () => $ReadOnlyArray<FocusPath> = () => {
165
- // eslint-disable-next-line react/no-string-refs
166
- return this.refs.renderer.getInputPaths();
167
- };
168
-
169
- setInputValue: (FocusPath, string, $FlowFixMe) => void = (
170
- path,
171
- newValue,
172
- cb,
173
- ) => {
174
- // eslint-disable-next-line react/no-string-refs
175
- return this.refs.renderer.setInputValue(path, newValue, cb);
176
- };
177
-
178
- focus: () => ?boolean = () => {
179
- // eslint-disable-next-line react/no-string-refs
180
- return this.refs.renderer.focus();
181
- };
182
-
183
- focusInputPath: (FocusPath) => void = (path) => {
184
- // eslint-disable-next-line react/no-string-refs
185
- this.refs.renderer.focusPath(path);
186
- };
187
-
188
- blurInputPath: (FocusPath) => void = (path) => {
189
- // eslint-disable-next-line react/no-string-refs
190
- this.refs.renderer.blurPath(path);
191
- };
192
-
193
- showRationalesForCurrentlySelectedChoices: () => void = () => {
194
- // eslint-disable-next-line react/no-string-refs
195
- this.refs.renderer.showRationalesForCurrentlySelectedChoices();
196
- };
197
179
  }
198
180
 
199
181
  const traverseChildWidgets = function (
200
182
  props: $FlowFixMe,
201
183
  traverseRenderer: $FlowFixMe,
202
184
  ): $FlowFixMe {
203
- return _.extend({}, props, traverseRenderer(props));
185
+ return {...props, ...traverseRenderer(props)};
204
186
  };
205
187
 
206
188
  export default ({
@@ -332,8 +332,13 @@ class InputNumber extends React.Component<Props> {
332
332
  if (rubric.answerType == null) {
333
333
  rubric.answerType = "number";
334
334
  }
335
+
336
+ // note(matthewc): this will get immediately parsed again by
337
+ // `KhanAnswerTypes.number.convertToPredicate`, but a string is
338
+ // expected here
339
+ const stringValue = `${rubric.value}`;
335
340
  const val = KhanAnswerTypes.number.createValidatorFunctional(
336
- rubric.value,
341
+ stringValue,
337
342
  {
338
343
  simplify: rubric.simplify,
339
344
  inexact: rubric.inexact || undefined,
@@ -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
  };
@@ -429,8 +439,7 @@ export class NumericInput extends React.Component<Props, State> {
429
439
  // TODO(charlie): Support "Review Mode".
430
440
  return maybeRightAlignKeypadInput(
431
441
  <SimpleKeypadInput
432
- // eslint-disable-next-line react/no-string-refs
433
- ref="input"
442
+ ref={(ref) => (this.inputRef = ref)}
434
443
  value={this.props.currentValue}
435
444
  keypadElement={this.props.keypadElement}
436
445
  onChange={this.handleChange}
@@ -442,8 +451,7 @@ export class NumericInput extends React.Component<Props, State> {
442
451
 
443
452
  const input = (
444
453
  <InputWithExamples
445
- // eslint-disable-next-line react/no-string-refs
446
- ref="input"
454
+ ref={(ref) => (this.inputRef = ref)}
447
455
  value={this.props.currentValue}
448
456
  onChange={this.handleChange}
449
457
  className={classNames(classes)}
@@ -493,7 +501,9 @@ export class NumericInput extends React.Component<Props, State> {
493
501
  // a given *problem* rather than for each possible [correct/wrong] *answer*.
494
502
  // When should two answers to a problem take different answer types?
495
503
  // See D27790 for more discussion.
496
- export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
504
+ export const unionAnswerForms: (
505
+ $ReadOnlyArray<$ReadOnlyArray<PerseusNumericInputAnswerForm>>,
506
+ ) => $ReadOnlyArray<PerseusNumericInputAnswerForm> = function (
497
507
  answerFormsList,
498
508
  ) {
499
509
  // Takes a list of lists of answer forms, and returns a list of the forms
@@ -504,28 +514,25 @@ export const unionAnswerForms: ($FlowFixMe) => $FlowFixMe = function (
504
514
  // two elements are equal, and returns a list of unique elements. This is
505
515
  // just a helper function here, but works generally.
506
516
  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
- );
517
+ return list.reduce((uniqueList, element) => {
518
+ // For each element, decide whether it's already in the list of
519
+ // unique items.
520
+ const inList = _.find(uniqueList, iteratee.bind(null, element));
521
+ if (inList) {
522
+ return uniqueList;
523
+ }
524
+ return uniqueList.concat([element]);
525
+ }, []);
520
526
  };
521
527
 
522
528
  // Pull out all of the forms from the different lists.
523
- const allForms = _.flatten(answerFormsList);
529
+ const allForms = answerFormsList.flat();
524
530
  // Pull out the unique forms using uniqueBy.
525
531
  const uniqueForms = uniqueBy(allForms, _.isEqual);
526
532
  // Sort them by the order they appear in the `formExamples` list.
533
+ const formExampleKeys = Object.keys(formExamples);
527
534
  return _.sortBy(uniqueForms, (form) => {
528
- return _.keys(formExamples).indexOf(form.name);
535
+ return formExampleKeys.indexOf(form.name);
529
536
  });
530
537
  };
531
538
 
@@ -547,10 +554,10 @@ type RenderProps = {|
547
554
  // can accept several input forms with or without "%", the decision
548
555
  // to parse based on the presence of "%" in the input, is so that we
549
556
  // don't accidently scale the user typed value before grading, CP-930.
550
- export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
551
- inputValue,
552
- normalizedAnswerExpected,
553
- ) => {
557
+ export function maybeParsePercentInput(
558
+ inputValue: string | number,
559
+ normalizedAnswerExpected: boolean,
560
+ ): string | number {
554
561
  // If the input value is not a string ending with "%", then there's
555
562
  // nothing more to do. The value will be graded as inputted by user.
556
563
  if (!(typeof inputValue === "string" && inputValue.endsWith("%"))) {
@@ -575,7 +582,7 @@ export const maybeParsePercentInput: ($FlowFixMe, $FlowFixMe) => $FlowFixMe = (
575
582
 
576
583
  // Otherwise, we return input valuåe (number) stripped of the "%".
577
584
  return value;
578
- };
585
+ }
579
586
 
580
587
  const propsTransform = function (
581
588
  widgetOptions: PerseusNumericInputWidgetOptions,
@@ -584,7 +591,7 @@ const propsTransform = function (
584
591
  answerForms: unionAnswerForms(
585
592
  // Pull out the name of each form and whether that form has
586
593
  // required simplification.
587
- _.map(widgetOptions.answers, (answer) => {
594
+ widgetOptions.answers.map((answer) => {
588
595
  return _.map(answer.answerForms, (form) => {
589
596
  return {
590
597
  simplify: answer.simplify,
@@ -7,7 +7,6 @@ import _ from "underscore";
7
7
  import PassageMarkdown from "../passage-markdown.jsx";
8
8
 
9
9
  const parse = PassageMarkdown.parse;
10
- const rules = PassageMarkdown._rulesForTesting;
11
10
 
12
11
  const validateParse = (parsed, expected) => {
13
12
  if (!_.isEqual(parsed, expected)) {
@@ -352,14 +351,6 @@ describe.skip("passage markdown", () => {
352
351
  });
353
352
  });
354
353
 
355
- describe("orders", () => {
356
- it("should always be numbers", () => {
357
- _.each(rules, (rule) => {
358
- expect(typeof rule.order === "number").toBeTruthy();
359
- });
360
- });
361
- });
362
-
363
354
  describe("output", () => {
364
355
  it("should output some basic formatted passage text", () => {
365
356
  assertParsesToReact(
@@ -1,10 +1,9 @@
1
1
  // @flow
2
-
3
- type JQueryCollection = $FlowFixMe;
2
+ import $ from "jquery";
4
3
 
5
4
  export const getLineHeightForNode = (
6
- $line1: JQueryCollection,
7
- $line2: JQueryCollection,
5
+ line1: HTMLDivElement,
6
+ line2: HTMLDivElement,
8
7
  ): number => {
9
- return $line2.offset().top - $line1.offset().top;
8
+ return $(line2).offset().top - $(line1).offset().top;
10
9
  };