@khanacademy/perseus 1.3.2 → 1.3.4

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 (39) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/es/index.css +12 -12
  3. package/dist/es/index.js +32 -26
  4. package/dist/es/index.js.map +1 -1
  5. package/dist/index.css +12 -12
  6. package/dist/index.js +32 -26
  7. package/dist/index.js.map +1 -1
  8. package/package.json +1 -1
  9. package/src/__tests__/__snapshots__/perseus-markdown_test.jsx.snap +111 -111
  10. package/src/__tests__/item-renderer_test.jsx +9 -9
  11. package/src/__tests__/jipt-paragraphs_test.js +3 -3
  12. package/src/__tests__/renderer_test.jsx +22 -22
  13. package/src/__tests__/server-item-renderer_test.jsx +7 -7
  14. package/src/components/__tests__/sortable_test.jsx +7 -7
  15. package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +5 -5
  16. package/src/multi-items/__tests__/multi-renderer_test.js +90 -90
  17. package/src/renderer.jsx +2 -2
  18. package/src/styles/articles.less +6 -6
  19. package/src/widgets/__tests__/__snapshots__/grapher_test.jsx.snap +68 -68
  20. package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +5 -5
  21. package/src/widgets/__tests__/__snapshots__/interaction_test.jsx.snap +5 -5
  22. package/src/widgets/__tests__/__snapshots__/interactive-graph_test.jsx.snap +2 -2
  23. package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +2 -2
  24. package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +52 -52
  25. package/src/widgets/__tests__/categorizer_test.jsx +5 -5
  26. package/src/widgets/__tests__/explanation_test.js +5 -5
  27. package/src/widgets/__tests__/graded-group-set_test.jsx +2 -2
  28. package/src/widgets/__tests__/group_test.jsx +49 -49
  29. package/src/widgets/__tests__/numeric-input_test.jsx +5 -5
  30. package/src/widgets/__tests__/passage_test.jsx +1 -1
  31. package/src/widgets/__tests__/radio/base-radio_test.jsx +28 -1
  32. package/src/widgets/categorizer.jsx +1 -1
  33. package/src/widgets/expression.jsx +6 -6
  34. package/src/widgets/input-number.jsx +4 -4
  35. package/src/widgets/numeric-input.jsx +5 -5
  36. package/src/widgets/radio/base-radio.jsx +8 -3
  37. package/src/widgets/radio/choice-icon/library-choice-icon.jsx +2 -2
  38. package/src/widgets/radio/choice.jsx +1 -1
  39. package/src/widgets/radio/widget.jsx +2 -2
@@ -169,10 +169,10 @@ describe("group widget", () => {
169
169
 
170
170
  // Assert
171
171
  expect(userInput).toMatchInlineSnapshot(`
172
- Array [
173
- Array [
174
- Object {
175
- "choicesSelected": Array [
172
+ [
173
+ [
174
+ {
175
+ "choicesSelected": [
176
176
  false,
177
177
  false,
178
178
  false,
@@ -181,11 +181,11 @@ describe("group widget", () => {
181
181
  ],
182
182
  },
183
183
  ],
184
- Array [
185
- Object {
184
+ [
185
+ {
186
186
  "currentValue": "99",
187
187
  },
188
- Object {
188
+ {
189
189
  "currentValue": "",
190
190
  },
191
191
  null,
@@ -204,32 +204,32 @@ describe("group widget", () => {
204
204
 
205
205
  // Assert
206
206
  expect(state).toMatchInlineSnapshot(`
207
- Object {
208
- "group 1": Object {
209
- "radio 1": Object {
210
- "choices": Array [
211
- Object {
207
+ {
208
+ "group 1": {
209
+ "radio 1": {
210
+ "choices": [
211
+ {
212
212
  "content": "$45$",
213
213
  "correct": false,
214
214
  "originalIndex": 0,
215
215
  },
216
- Object {
216
+ {
217
217
  "content": "$42$",
218
218
  "correct": false,
219
219
  "originalIndex": 1,
220
220
  },
221
- Object {
221
+ {
222
222
  "clue": "Here's a clue, this isn't the correct answer!",
223
223
  "content": "$30$",
224
224
  "correct": false,
225
225
  "originalIndex": 2,
226
226
  },
227
- Object {
227
+ {
228
228
  "content": "$18$",
229
229
  "correct": false,
230
230
  "originalIndex": 3,
231
231
  },
232
- Object {
232
+ {
233
233
  "content": "$15$",
234
234
  "correct": true,
235
235
  "originalIndex": 4,
@@ -240,7 +240,7 @@ describe("group widget", () => {
240
240
  "hasNoneOfTheAbove": false,
241
241
  "multipleSelect": false,
242
242
  "numCorrect": 1,
243
- "selectedChoices": Array [
243
+ "selectedChoices": [
244
244
  false,
245
245
  false,
246
246
  false,
@@ -249,26 +249,26 @@ describe("group widget", () => {
249
249
  ],
250
250
  },
251
251
  },
252
- "group 2": Object {
253
- "image 1": Object {
252
+ "group 2": {
253
+ "image 1": {
254
254
  "alt": "A number line labeled 200 to 300 with tick marks at every 5 units. The tick marks at 200, 210, 220, 230, 240, 250, 260, 270, 280, 290, and 300 are labeled. A red circle labeled A is between 220 tick mark and 230 tick mark.",
255
- "backgroundImage": Object {
255
+ "backgroundImage": {
256
256
  "height": 80,
257
257
  "url": "web+graphie://ka-perseus-graphie.s3.amazonaws.com/3351ccf19e60c28a1d08664f5c16defa76ed0348",
258
258
  "width": 380,
259
259
  },
260
- "box": Array [
260
+ "box": [
261
261
  380,
262
262
  80,
263
263
  ],
264
264
  "caption": "",
265
- "labels": Array [],
266
- "range": Array [
267
- Array [
265
+ "labels": [],
266
+ "range": [
267
+ [
268
268
  0,
269
269
  10,
270
270
  ],
271
- Array [
271
+ [
272
272
  0,
273
273
  10,
274
274
  ],
@@ -276,8 +276,8 @@ describe("group widget", () => {
276
276
  "static": false,
277
277
  "title": "",
278
278
  },
279
- "numeric-input 1": Object {
280
- "answerForms": Array [],
279
+ "numeric-input 1": {
280
+ "answerForms": [],
281
281
  "coefficient": false,
282
282
  "currentValue": "99",
283
283
  "labelText": "value rounded to the nearest ten",
@@ -286,8 +286,8 @@ describe("group widget", () => {
286
286
  "size": "normal",
287
287
  "static": false,
288
288
  },
289
- "numeric-input 2": Object {
290
- "answerForms": Array [],
289
+ "numeric-input 2": {
290
+ "answerForms": [],
291
291
  "coefficient": false,
292
292
  "labelText": "value rounded to the nearest hundred",
293
293
  "multipleNumberInput": false,
@@ -296,29 +296,29 @@ describe("group widget", () => {
296
296
  "static": false,
297
297
  },
298
298
  },
299
- "radio 1": Object {
300
- "choices": Array [
301
- Object {
299
+ "radio 1": {
300
+ "choices": [
301
+ {
302
302
  "content": "",
303
303
  "correct": false,
304
304
  "originalIndex": 0,
305
305
  },
306
- Object {
306
+ {
307
307
  "content": "",
308
308
  "correct": false,
309
309
  "originalIndex": 1,
310
310
  },
311
- Object {
311
+ {
312
312
  "content": "",
313
313
  "correct": false,
314
314
  "originalIndex": 2,
315
315
  },
316
- Object {
316
+ {
317
317
  "content": "",
318
318
  "correct": false,
319
319
  "originalIndex": 3,
320
320
  },
321
- Object {
321
+ {
322
322
  "content": "",
323
323
  "correct": true,
324
324
  "originalIndex": 4,
@@ -329,7 +329,7 @@ describe("group widget", () => {
329
329
  "hasNoneOfTheAbove": false,
330
330
  "multipleSelect": false,
331
331
  "numCorrect": 1,
332
- "selectedChoices": Array [
332
+ "selectedChoices": [
333
333
  false,
334
334
  false,
335
335
  false,
@@ -377,11 +377,11 @@ describe("group widget", () => {
377
377
 
378
378
  // Assert
379
379
  expect(guessAndScore).toMatchInlineSnapshot(`
380
- Array [
381
- Array [
382
- Array [
383
- Object {
384
- "choicesSelected": Array [
380
+ [
381
+ [
382
+ [
383
+ {
384
+ "choicesSelected": [
385
385
  false,
386
386
  false,
387
387
  false,
@@ -394,17 +394,17 @@ describe("group widget", () => {
394
394
  "numCorrect": 1,
395
395
  },
396
396
  ],
397
- Array [
398
- Object {
397
+ [
398
+ {
399
399
  "currentValue": "230",
400
400
  },
401
- Object {
401
+ {
402
402
  "currentValue": "200",
403
403
  },
404
404
  null,
405
405
  ],
406
406
  ],
407
- Object {
407
+ {
408
408
  "earned": 3,
409
409
  "message": null,
410
410
  "total": 3,
@@ -423,12 +423,12 @@ describe("group widget", () => {
423
423
 
424
424
  // Assert
425
425
  expect(paths).toMatchInlineSnapshot(`
426
- Array [
427
- Array [
426
+ [
427
+ [
428
428
  "group 2",
429
429
  "numeric-input 1",
430
430
  ],
431
- Array [
431
+ [
432
432
  "group 2",
433
433
  "numeric-input 2",
434
434
  ],
@@ -218,7 +218,7 @@ describe("static function validate", () => {
218
218
  const score = NumericInput.validate(useInput, rubric);
219
219
 
220
220
  expect(score).toMatchInlineSnapshot(`
221
- Object {
221
+ {
222
222
  "earned": 1,
223
223
  "message": null,
224
224
  "total": 1,
@@ -252,7 +252,7 @@ describe("static function validate", () => {
252
252
  const score = NumericInput.validate(useInput, rubric);
253
253
 
254
254
  expect(score).toMatchInlineSnapshot(`
255
- Object {
255
+ {
256
256
  "message": "We could not understand your answer. Please check your answer for extra text or symbols.",
257
257
  "type": "invalid",
258
258
  }
@@ -284,7 +284,7 @@ describe("static function validate", () => {
284
284
  const score = NumericInput.validate(useInput, rubric);
285
285
 
286
286
  expect(score).toMatchInlineSnapshot(`
287
- Object {
287
+ {
288
288
  "earned": 1,
289
289
  "message": null,
290
290
  "total": 1,
@@ -318,7 +318,7 @@ describe("static function validate", () => {
318
318
  const score = NumericInput.validate(useInput, rubric);
319
319
 
320
320
  expect(score).toMatchInlineSnapshot(`
321
- Object {
321
+ {
322
322
  "earned": 0,
323
323
  "message": null,
324
324
  "total": 1,
@@ -352,7 +352,7 @@ describe("static function validate", () => {
352
352
  const score = NumericInput.validate(useInput, rubric);
353
353
 
354
354
  expect(score).toMatchInlineSnapshot(`
355
- Object {
355
+ {
356
356
  "earned": 1,
357
357
  "message": null,
358
358
  "total": 1,
@@ -116,7 +116,7 @@ describe("passage widget", () => {
116
116
 
117
117
  // Assert
118
118
  expect(score).toMatchInlineSnapshot(`
119
- Object {
119
+ {
120
120
  "earned": 0,
121
121
  "message": null,
122
122
  "total": 0,
@@ -132,7 +132,7 @@ describe("base-radio", () => {
132
132
  expect(screen.getAllByRole("listitem")).toHaveLength(4);
133
133
  });
134
134
 
135
- it("should toggle choice when inner element clicked", () => {
135
+ it("should not toggle choice when inner element clicked", () => {
136
136
  // Arrange
137
137
  let updatedValues = null;
138
138
  const onChangeHandler = (newValues) => {
@@ -155,6 +155,33 @@ describe("base-radio", () => {
155
155
  screen.getByRole("radio", {name: "(Choice C) Option Gamma"}),
156
156
  );
157
157
 
158
+ // Assert
159
+ expect(updatedValues).toBeNull();
160
+ });
161
+
162
+ it("should toggle choice when choice icon clicked", () => {
163
+ // Arrange
164
+ let updatedValues = null;
165
+ const onChangeHandler = (newValues) => {
166
+ updatedValues = newValues;
167
+ };
168
+
169
+ renderBaseRadio({
170
+ editMode: true,
171
+ choices: [
172
+ generateChoice({content: "Option 1", correct: false}),
173
+ generateChoice({content: "Option B", correct: false}),
174
+ generateChoice({content: "Option Gamma", correct: true}),
175
+ generateChoice({content: "Option Delta", correct: false}),
176
+ ],
177
+ onChange: onChangeHandler,
178
+ });
179
+
180
+ // Act
181
+ userEvent.click(
182
+ screen.getAllByTestId("choice-icon__library-choice-icon")[2],
183
+ );
184
+
158
185
  // Assert
159
186
  expect(updatedValues).toMatchObject({
160
187
  checked: [false, false, true, false],
@@ -257,7 +257,7 @@ export class Categorizer extends React.Component<Props, State> {
257
257
  const pageMargin = 16;
258
258
 
259
259
  // Stylesheets aren't directly testable
260
- /* istanbul ignore next */
260
+ /* c8 ignore next */
261
261
  const styles = StyleSheet.create({
262
262
  mobileTable: {
263
263
  [mediaQueries.smOrSmaller]: {
@@ -122,7 +122,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
122
122
  // incorrectly. For example if the answer is using a symbol defined
123
123
  // in the function variables list for the expression.
124
124
  if (!expression.parsed) {
125
- /* istanbul ignore next */
125
+ /* c8 ignore next */
126
126
  Log.error(
127
127
  "Unable to parse solution answer for expression",
128
128
  PerseusErrors.InvalidInput,
@@ -365,12 +365,12 @@ export class Expression extends React.Component<Props, ExpressionState> {
365
365
  };
366
366
 
367
367
  _handleFocus: () => void = () => {
368
- /* istanbul ignore next */
368
+ /* c8 ignore next */
369
369
  this.props.onFocus([]);
370
370
  };
371
371
 
372
372
  _handleBlur: () => void = () => {
373
- /* istanbul ignore next */
373
+ /* c8 ignore next */
374
374
  this.props.onBlur([]);
375
375
  };
376
376
 
@@ -407,7 +407,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
407
407
  getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<any>> = () => {
408
408
  // The widget itself is an input, so we return a single empty list to
409
409
  // indicate this.
410
- /* istanbul ignore next */
410
+ /* c8 ignore next */
411
411
  return [[]];
412
412
  };
413
413
 
@@ -415,7 +415,7 @@ export class Expression extends React.Component<Props, ExpressionState> {
415
415
  getGrammarTypeForPath: (inputPath: string) => string = (
416
416
  inputPath: string,
417
417
  ) => {
418
- /* istanbul ignore next */
418
+ /* c8 ignore next */
419
419
  return "expression";
420
420
  };
421
421
 
@@ -600,7 +600,7 @@ const keypadConfigurationForProps = (
600
600
  };
601
601
 
602
602
  const propUpgrades = {
603
- /* istanbul ignore next */
603
+ /* c8 ignore next */
604
604
  "1": (v0props: $FlowFixMe): PerseusExpressionWidgetOptions => ({
605
605
  times: v0props.times,
606
606
  buttonSets: v0props.buttonSets,
@@ -170,7 +170,7 @@ class InputNumber extends React.Component<Props> {
170
170
  let answerBlurb = null;
171
171
 
172
172
  // SAT is deprecated and not testable
173
- /* istanbul ignore if */
173
+ /* c8 ignore if */
174
174
  if (this.props.apiOptions.satStyling && rubric) {
175
175
  const score = this.simpleValidate(rubric);
176
176
  correct = score.type === "points" && score.earned === score.total;
@@ -215,7 +215,7 @@ class InputNumber extends React.Component<Props> {
215
215
  );
216
216
 
217
217
  // SAT is deprecated, answer blurb is an SAT feature
218
- /* istanbul ignore if */
218
+ /* c8 ignore if */
219
219
  if (answerBlurb) {
220
220
  return (
221
221
  <span className="perseus-input-with-answer-blurb">
@@ -272,14 +272,14 @@ class InputNumber extends React.Component<Props> {
272
272
  getInputPaths: () => $ReadOnlyArray<Path> = () => {
273
273
  // The widget itself is an input, so we return a single empty list to
274
274
  // indicate this.
275
- /* istanbul ignore next */
275
+ /* c8 ignore next */
276
276
  return [[]];
277
277
  };
278
278
 
279
279
  // Note: We believe that this isn't used anywhere but are leaving it during
280
280
  // the initial refactor
281
281
  getGrammarTypeForPath: (Path) => string = (path) => {
282
- /* istanbul ignore next */
282
+ /* c8 ignore next */
283
283
  return "number";
284
284
  };
285
285
 
@@ -267,7 +267,7 @@ export class NumericInput extends React.Component<Props, State> {
267
267
  let answerBlurb;
268
268
 
269
269
  /* Ignore this block because SAT is deprecated */
270
- /* istanbul ignore next */
270
+ /* c8 ignore next */
271
271
  if (this.props.apiOptions.satStyling && rubric) {
272
272
  const score = this.simpleValidate(rubric);
273
273
  correct = score.type === "points" && score.earned === score.total;
@@ -361,12 +361,12 @@ export class NumericInput extends React.Component<Props, State> {
361
361
  getInputPaths: () => $ReadOnlyArray<$ReadOnlyArray<string>> = () => {
362
362
  // The widget itself is an input, so we return a single empty list to
363
363
  // indicate this.
364
- /* istanbul ignore next */
364
+ /* c8 ignore next */
365
365
  return [[]];
366
366
  };
367
367
 
368
368
  getGrammarTypeForPath: (FocusPath) => string = (inputPath) => {
369
- /* istanbul ignore next */
369
+ /* c8 ignore next */
370
370
  return "number";
371
371
  };
372
372
 
@@ -375,7 +375,7 @@ export class NumericInput extends React.Component<Props, State> {
375
375
  newValue,
376
376
  cb,
377
377
  ) => {
378
- /* istanbul ignore next */
378
+ /* c8 ignore next */
379
379
  this.props.onChange(
380
380
  {
381
381
  currentValue: newValue,
@@ -475,7 +475,7 @@ export class NumericInput extends React.Component<Props, State> {
475
475
  );
476
476
 
477
477
  /* This only gets used with SAT Content */
478
- /* istanbul ignore next if */
478
+ /* c8 ignore next if */
479
479
  if (answerBlurb) {
480
480
  return (
481
481
  <span className="perseus-input-with-answer-blurb">
@@ -256,7 +256,7 @@ function BaseRadio(props: Props): React.Node {
256
256
  </div>
257
257
  )}
258
258
  <ul className={className} style={{listStyle: "none"}}>
259
- {choices.map(function (choice, i) {
259
+ {choices.map((choice, i) => {
260
260
  let Element = Choice;
261
261
  const ref = React.createRef();
262
262
  choiceRefs.current[i] = ref;
@@ -277,6 +277,11 @@ function BaseRadio(props: Props): React.Node {
277
277
  (reviewMode || choice.showRationale),
278
278
  pos: i,
279
279
  onChange: (newValues) => {
280
+ // editMode selection is handled in clickHandler
281
+ if (editMode) {
282
+ return;
283
+ }
284
+
280
285
  updateChoice(i, newValues);
281
286
  },
282
287
  ref,
@@ -379,7 +384,7 @@ function BaseRadio(props: Props): React.Node {
379
384
  // radio icon, then we want to trigger the
380
385
  // check by flipping the choice of the icon.
381
386
  if (elem.getAttribute("data-is-radio-icon")) {
382
- this.updateChoice(i, {
387
+ updateChoice(i, {
383
388
  checked: !choice.checked,
384
389
  crossedOut: choice.crossedOut,
385
390
  });
@@ -408,7 +413,7 @@ function BaseRadio(props: Props): React.Node {
408
413
  <Element {...elementProps} />
409
414
  </li>
410
415
  );
411
- }, this)}
416
+ })}
412
417
  </ul>
413
418
  </fieldset>
414
419
  );
@@ -149,8 +149,8 @@ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
149
149
  (checked || previouslyAnswered) &&
150
150
  styles.circleIncorrectAnswered,
151
151
  )}
152
- // used in BaseRadio to check if we actually clicked on the
153
- // radio icon
152
+ // used in BaseRadio editMode to check
153
+ // if we actually clicked on the radio icon
154
154
  data-is-radio-icon={true}
155
155
  >
156
156
  <div className={css(styles.innerWrapper)}>
@@ -228,7 +228,7 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
228
228
  apiOptions.staticRender ||
229
229
  apiOptions.readOnly
230
230
  }
231
- style={{flex: 1}}
231
+ style={{flex: 1, color: Color.offBlack}}
232
232
  ref={(forwardedRef: any)}
233
233
  aria-hidden="true"
234
234
  >
@@ -157,7 +157,7 @@ class Radio extends React.Component<Props> {
157
157
  };
158
158
  // Support legacy choiceState implementation
159
159
  }
160
- /* istanbul ignore if - props.values is deprecated */
160
+ /* c8 ignore if - props.values is deprecated */
161
161
  const {values} = props;
162
162
  if (values) {
163
163
  let noneOfTheAboveIndex = null;
@@ -414,7 +414,7 @@ class Radio extends React.Component<Props> {
414
414
  choiceStates = this.props.choiceStates;
415
415
  } else if (this.props.values) {
416
416
  // Support legacy choiceStates implementation
417
- /* istanbul ignore next - props.values is deprecated */
417
+ /* c8 ignore next - props.values is deprecated */
418
418
  choiceStates = this.props.values.map((val) => ({
419
419
  selected: val,
420
420
  crossedOut: false,