@khanacademy/perseus 1.1.0 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/es/index.css +3 -0
- package/dist/es/index.js +15385 -15345
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.js +15309 -15269
- package/dist/index.js.map +1 -1
- package/dist/strings.js +1 -0
- package/package.json +1 -1
- package/src/__stories__/hints-renderer.stories.jsx +52 -0
- package/src/components/__stories__/input-with-examples.stories.jsx +1 -0
- package/src/components/input-with-examples.jsx +41 -24
- package/src/components/text-input.jsx +5 -3
- package/src/hints-renderer.jsx +13 -4
- package/src/mixins/__tests__/widget-prop-blacklist_test.js +23 -0
- package/src/mixins/widget-prop-blacklist.js +15 -1
- package/src/multi-items/__tests__/__snapshots__/multi-renderer_test.js.snap +30 -10
- package/src/styles/widgets/matrix.less +2 -0
- package/src/styles/widgets/passage.less +1 -0
- package/src/types.js +1 -1
- package/src/widgets/__stories__/numeric-input.stories.jsx +53 -7
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +18 -9
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +12 -3
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +6 -3
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +32 -12
- package/src/widgets/__tests__/__snapshots__/matrix_test.jsx.snap +24 -24
- package/src/widgets/__tests__/__snapshots__/numeric-input_test.jsx.snap +4 -4
- package/src/widgets/__tests__/__snapshots__/passage-ref_test.jsx.snap +3 -3
- package/src/widgets/__tests__/__snapshots__/passage_test.jsx.snap +6 -6
- package/src/widgets/__tests__/__snapshots__/radio_test.jsx.snap +300 -218
- package/src/widgets/__tests__/radio/base-radio_test.jsx +90 -6
- package/src/widgets/__tests__/radio/choice-icon_test.jsx +74 -0
- package/src/widgets/__tests__/radio/choice_test.jsx +39 -30
- package/src/widgets/__tests__/radio_test.jsx +228 -22
- package/src/widgets/graded-group-set.jsx +8 -1
- package/src/widgets/graded-group.jsx +7 -2
- package/src/widgets/matrix.jsx +17 -1
- package/src/widgets/numeric-input.jsx +12 -6
- package/src/widgets/passage-ref.jsx +21 -5
- package/src/widgets/passage.jsx +3 -1
- package/src/widgets/radio/__stories__/base-radio.stories.jsx +7 -3
- package/src/widgets/radio/base-radio.jsx +3 -1
- package/src/widgets/radio/choice-icon/cross-out-line.jsx +3 -2
- package/src/widgets/radio/choice-icon/library-choice-icon.jsx +17 -6
- package/src/widgets/radio/choice.jsx +2 -1
|
@@ -180,14 +180,19 @@ describe("single-choice question", () => {
|
|
|
180
180
|
|
|
181
181
|
// Assert
|
|
182
182
|
// Everything's read-only so no selections made
|
|
183
|
-
// Note(TB): The visual buttons are hidden from screen
|
|
184
|
-
// so they need to be identified as hidden
|
|
185
|
-
//
|
|
183
|
+
// Note(TB): The visual buttons are hidden from screen
|
|
184
|
+
// readers, so they need to be identified as hidden;
|
|
185
|
+
// the visual button has the aria attributes
|
|
186
186
|
screen
|
|
187
187
|
.getAllByRole("button", {hidden: true})
|
|
188
188
|
.forEach((r) => {
|
|
189
189
|
expect(r).toHaveAttribute("aria-disabled", "true");
|
|
190
190
|
});
|
|
191
|
+
// Note(TB): Confirms the screen reader only
|
|
192
|
+
// radio options are also disabled
|
|
193
|
+
screen.getAllByRole("radio").forEach((r) => {
|
|
194
|
+
expect(r).toHaveAttribute("disabled");
|
|
195
|
+
});
|
|
191
196
|
});
|
|
192
197
|
},
|
|
193
198
|
);
|
|
@@ -198,15 +203,12 @@ describe("single-choice question", () => {
|
|
|
198
203
|
|
|
199
204
|
// Act
|
|
200
205
|
userEvent.tab();
|
|
201
|
-
// Note(TB): The visual buttons are hidden from screen readers
|
|
202
|
-
// so they need to be identified as hidden;
|
|
203
|
-
// cannot access screen reader buttons via tabbing
|
|
204
|
-
expect(
|
|
205
|
-
screen.getAllByRole("button", {hidden: true})[0],
|
|
206
|
-
).toHaveFocus();
|
|
207
206
|
userEvent.tab();
|
|
208
207
|
|
|
209
208
|
// Assert
|
|
209
|
+
// Note(TB): The visual buttons are hidden from screen readers
|
|
210
|
+
// so they need to be identified as hidden;
|
|
211
|
+
// cannot access screen reader buttons via tabbing
|
|
210
212
|
expect(
|
|
211
213
|
screen.getAllByRole("button", {hidden: true})[1],
|
|
212
214
|
).toHaveFocus();
|
|
@@ -237,7 +239,19 @@ describe("single-choice question", () => {
|
|
|
237
239
|
).toHaveFocus();
|
|
238
240
|
});
|
|
239
241
|
|
|
240
|
-
it("should be able to
|
|
242
|
+
it("should be able to select an option by keyboard", () => {
|
|
243
|
+
// Arrange
|
|
244
|
+
renderQuestion(question, apiOptions);
|
|
245
|
+
|
|
246
|
+
// Act
|
|
247
|
+
userEvent.tab();
|
|
248
|
+
userEvent.keyboard("{space}");
|
|
249
|
+
|
|
250
|
+
// Assert
|
|
251
|
+
expect(screen.getAllByRole("radio")[0]).toBeChecked();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should be able to navigate to 'None of the above' choice by keyboard", () => {
|
|
241
255
|
// Arrange
|
|
242
256
|
const q = clone(question);
|
|
243
257
|
((q.widgets["radio 1"]
|
|
@@ -336,7 +350,7 @@ describe("single-choice question", () => {
|
|
|
336
350
|
expect(passageRefRadio).toHaveTextContent("lines NaN–NaN");
|
|
337
351
|
});
|
|
338
352
|
|
|
339
|
-
it("
|
|
353
|
+
it("should render rationales for selected choices", () => {
|
|
340
354
|
// Arrange
|
|
341
355
|
const {renderer} = renderQuestion(question, apiOptions);
|
|
342
356
|
|
|
@@ -359,9 +373,7 @@ describe("single-choice question", () => {
|
|
|
359
373
|
};
|
|
360
374
|
|
|
361
375
|
it("should render cross-out menu button", () => {
|
|
362
|
-
// Arrange
|
|
363
|
-
|
|
364
|
-
// Act
|
|
376
|
+
// Arrange & Act
|
|
365
377
|
renderQuestion(question, crossOutApiOptions);
|
|
366
378
|
|
|
367
379
|
// Assert
|
|
@@ -374,7 +386,6 @@ describe("single-choice question", () => {
|
|
|
374
386
|
|
|
375
387
|
it("should open the cross-out menu when button clicked", async () => {
|
|
376
388
|
// Arrange
|
|
377
|
-
|
|
378
389
|
renderQuestion(question, crossOutApiOptions);
|
|
379
390
|
|
|
380
391
|
// Act
|
|
@@ -397,7 +408,6 @@ describe("single-choice question", () => {
|
|
|
397
408
|
|
|
398
409
|
it("should open the cross-out menu when focused and spacebar pressed", async () => {
|
|
399
410
|
// Arrange
|
|
400
|
-
|
|
401
411
|
renderQuestion(question, crossOutApiOptions);
|
|
402
412
|
userEvent.tab(); // Choice icon
|
|
403
413
|
userEvent.tab(); // Cross-out menu ellipsis
|
|
@@ -416,7 +426,7 @@ describe("single-choice question", () => {
|
|
|
416
426
|
).toBeVisible();
|
|
417
427
|
});
|
|
418
428
|
|
|
419
|
-
it("should cross-out selection and dismiss button when clicked",
|
|
429
|
+
it("should cross-out selection and dismiss button when clicked", () => {
|
|
420
430
|
// Arrange
|
|
421
431
|
renderQuestion(question, crossOutApiOptions);
|
|
422
432
|
userEvent.click(
|
|
@@ -424,9 +434,6 @@ describe("single-choice question", () => {
|
|
|
424
434
|
name: /Open menu for Choice B/,
|
|
425
435
|
}),
|
|
426
436
|
);
|
|
427
|
-
await act(async () => {
|
|
428
|
-
await jest.runAllTimers();
|
|
429
|
-
});
|
|
430
437
|
|
|
431
438
|
// Act
|
|
432
439
|
userEvent.click(
|
|
@@ -434,7 +441,6 @@ describe("single-choice question", () => {
|
|
|
434
441
|
name: /Cross out Choice B/,
|
|
435
442
|
}),
|
|
436
443
|
);
|
|
437
|
-
jest.runAllTimers();
|
|
438
444
|
|
|
439
445
|
// Assert
|
|
440
446
|
expect(
|
|
@@ -442,11 +448,43 @@ describe("single-choice question", () => {
|
|
|
442
448
|
name: /Cross out Choice B/,
|
|
443
449
|
}),
|
|
444
450
|
).toHaveLength(0);
|
|
451
|
+
|
|
452
|
+
expect(
|
|
453
|
+
screen.getByTestId("choice-icon__cross-out-line"),
|
|
454
|
+
).toBeVisible();
|
|
445
455
|
});
|
|
446
456
|
|
|
447
|
-
it("should
|
|
457
|
+
it("should remove cross-out line on selection", async () => {
|
|
448
458
|
// Arrange
|
|
459
|
+
renderQuestion(question, crossOutApiOptions);
|
|
460
|
+
userEvent.click(
|
|
461
|
+
screen.getByRole("button", {
|
|
462
|
+
name: /Open menu for Choice B/,
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
|
|
466
|
+
// Act
|
|
467
|
+
userEvent.click(
|
|
468
|
+
screen.getByRole("button", {
|
|
469
|
+
name: /Cross out Choice B/,
|
|
470
|
+
}),
|
|
471
|
+
);
|
|
472
|
+
jest.runAllTimers();
|
|
473
|
+
|
|
474
|
+
userEvent.click(
|
|
475
|
+
screen.getByRole("radio", {
|
|
476
|
+
name: "(Choice B, Crossed out) -8",
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
|
|
480
|
+
// Assert
|
|
481
|
+
expect(
|
|
482
|
+
screen.queryByTestId("choice-icon__cross-out-line"),
|
|
483
|
+
).not.toBeInTheDocument();
|
|
484
|
+
});
|
|
449
485
|
|
|
486
|
+
it("should dismiss cross-out button with {tab} key", async () => {
|
|
487
|
+
// Arrange
|
|
450
488
|
renderQuestion(question, crossOutApiOptions);
|
|
451
489
|
userEvent.tab(); // Choice icon
|
|
452
490
|
userEvent.tab(); // Cross-out menu ellipsis
|
|
@@ -476,6 +514,19 @@ describe("single-choice question", () => {
|
|
|
476
514
|
});
|
|
477
515
|
});
|
|
478
516
|
|
|
517
|
+
it("should not display instructions when satStyling true", () => {
|
|
518
|
+
// Arrange
|
|
519
|
+
const apiOptions: APIOptions = {
|
|
520
|
+
satStyling: true,
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
// Act
|
|
524
|
+
renderQuestion(question, apiOptions);
|
|
525
|
+
|
|
526
|
+
// Assert
|
|
527
|
+
expect(screen.queryByText("Choose 1 answer:")).not.toBeInTheDocument();
|
|
528
|
+
});
|
|
529
|
+
|
|
479
530
|
it("should be invalid when first rendered", () => {
|
|
480
531
|
// Arrange
|
|
481
532
|
const apiOptions: APIOptions = {
|
|
@@ -516,6 +567,60 @@ describe("single-choice question", () => {
|
|
|
516
567
|
// Assert
|
|
517
568
|
expect(screen.getAllByText("Incorrect (selected)")).toHaveLength(1);
|
|
518
569
|
});
|
|
570
|
+
|
|
571
|
+
it("should display all rationales when static is true", () => {
|
|
572
|
+
// Arrange
|
|
573
|
+
const staticQuestion = {
|
|
574
|
+
...question,
|
|
575
|
+
widgets: {
|
|
576
|
+
...question.widgets,
|
|
577
|
+
"radio 1": {
|
|
578
|
+
...question.widgets["radio 1"],
|
|
579
|
+
static: true,
|
|
580
|
+
},
|
|
581
|
+
},
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// Act
|
|
585
|
+
renderQuestion(staticQuestion);
|
|
586
|
+
|
|
587
|
+
// Assert
|
|
588
|
+
// Part of Choice A rationale
|
|
589
|
+
expect(
|
|
590
|
+
screen.getByText(
|
|
591
|
+
/the positive square root when performed on a number, so/,
|
|
592
|
+
),
|
|
593
|
+
).toBeVisible();
|
|
594
|
+
// Part of Choice B rationale
|
|
595
|
+
expect(screen.getByText(/, the square root operation/)).toBeVisible();
|
|
596
|
+
// Part of Choice C rationale
|
|
597
|
+
expect(
|
|
598
|
+
screen.getByText(/is the positive square root of/),
|
|
599
|
+
).toBeVisible();
|
|
600
|
+
// Part of Choice D rationale
|
|
601
|
+
expect(screen.getAllByText(/satisfies the equation./)[3]).toBeVisible();
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
it("should register as correct when none of the above option selected", () => {
|
|
605
|
+
// Arrange
|
|
606
|
+
const q = clone(question);
|
|
607
|
+
const choices = ((q.widgets["radio 1"]
|
|
608
|
+
.options: any): PerseusRadioWidgetOptions).choices;
|
|
609
|
+
choices[2].correct = false;
|
|
610
|
+
choices[3].isNoneOfTheAbove = true;
|
|
611
|
+
choices[3].correct = true;
|
|
612
|
+
|
|
613
|
+
const {renderer} = renderQuestion(q);
|
|
614
|
+
|
|
615
|
+
// Act
|
|
616
|
+
const noneOption = screen.getByRole("radio", {
|
|
617
|
+
name: "(Choice D) None of the above",
|
|
618
|
+
});
|
|
619
|
+
userEvent.click(noneOption);
|
|
620
|
+
|
|
621
|
+
// Assert
|
|
622
|
+
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
623
|
+
});
|
|
519
624
|
});
|
|
520
625
|
|
|
521
626
|
describe("multi-choice question", () => {
|
|
@@ -544,6 +649,94 @@ describe("multi-choice question", () => {
|
|
|
544
649
|
expect(renderer).toHaveBeenAnsweredCorrectly();
|
|
545
650
|
});
|
|
546
651
|
|
|
652
|
+
it("should select multiple options when clicked", () => {
|
|
653
|
+
// Arrange
|
|
654
|
+
const apiOptions: APIOptions = {
|
|
655
|
+
crossOutEnabled: false,
|
|
656
|
+
};
|
|
657
|
+
const radio1Widget = ((question.widgets["radio 1"]: any): RadioWidget);
|
|
658
|
+
const radioOptions = radio1Widget.options;
|
|
659
|
+
|
|
660
|
+
const multipleCorrectChoicesQuestion: PerseusRenderer = {
|
|
661
|
+
...question,
|
|
662
|
+
widgets: {
|
|
663
|
+
...question.widgets,
|
|
664
|
+
"radio 1": ({
|
|
665
|
+
...radio1Widget,
|
|
666
|
+
options: {
|
|
667
|
+
...radioOptions,
|
|
668
|
+
choices: [
|
|
669
|
+
{content: "$x=-6$", correct: true},
|
|
670
|
+
{content: "$x=4$", correct: true},
|
|
671
|
+
{content: "$x=7$", correct: false},
|
|
672
|
+
{
|
|
673
|
+
content: "There is no such input value.",
|
|
674
|
+
isNoneOfTheAbove: true,
|
|
675
|
+
correct: false,
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
},
|
|
679
|
+
}: RadioWidget),
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
renderQuestion(multipleCorrectChoicesQuestion, apiOptions);
|
|
684
|
+
|
|
685
|
+
// Act
|
|
686
|
+
const options = screen.getAllByRole("checkbox");
|
|
687
|
+
userEvent.click(options[2]);
|
|
688
|
+
userEvent.click(options[3]);
|
|
689
|
+
|
|
690
|
+
// Assert
|
|
691
|
+
expect(options[2]).toBeChecked();
|
|
692
|
+
expect(options[3]).toBeChecked();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
it("should deselect selected options when clicked", () => {
|
|
696
|
+
// Arrange
|
|
697
|
+
const apiOptions: APIOptions = {
|
|
698
|
+
crossOutEnabled: false,
|
|
699
|
+
};
|
|
700
|
+
const radio1Widget = ((question.widgets["radio 1"]: any): RadioWidget);
|
|
701
|
+
const radioOptions = radio1Widget.options;
|
|
702
|
+
|
|
703
|
+
const multipleCorrectChoicesQuestion: PerseusRenderer = {
|
|
704
|
+
...question,
|
|
705
|
+
widgets: {
|
|
706
|
+
...question.widgets,
|
|
707
|
+
"radio 1": ({
|
|
708
|
+
...radio1Widget,
|
|
709
|
+
options: {
|
|
710
|
+
...radioOptions,
|
|
711
|
+
choices: [
|
|
712
|
+
{content: "$x=-6$", correct: true},
|
|
713
|
+
{content: "$x=4$", correct: true},
|
|
714
|
+
{content: "$x=7$", correct: false},
|
|
715
|
+
{
|
|
716
|
+
content: "There is no such input value.",
|
|
717
|
+
isNoneOfTheAbove: true,
|
|
718
|
+
correct: false,
|
|
719
|
+
},
|
|
720
|
+
],
|
|
721
|
+
},
|
|
722
|
+
}: RadioWidget),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
renderQuestion(multipleCorrectChoicesQuestion, apiOptions);
|
|
727
|
+
|
|
728
|
+
// Act
|
|
729
|
+
const options = screen.getAllByRole("checkbox");
|
|
730
|
+
userEvent.click(options[2]);
|
|
731
|
+
userEvent.click(options[3]);
|
|
732
|
+
userEvent.click(options[2]);
|
|
733
|
+
userEvent.click(options[3]);
|
|
734
|
+
|
|
735
|
+
// Assert
|
|
736
|
+
expect(options[2]).not.toBeChecked();
|
|
737
|
+
expect(options[3]).not.toBeChecked();
|
|
738
|
+
});
|
|
739
|
+
|
|
547
740
|
it("should snapshot the same when invalid", () => {
|
|
548
741
|
// Arrange
|
|
549
742
|
const {container} = renderQuestion(question, apiOptions);
|
|
@@ -556,6 +749,19 @@ describe("multi-choice question", () => {
|
|
|
556
749
|
expect(container).toMatchSnapshot("invalid state");
|
|
557
750
|
});
|
|
558
751
|
|
|
752
|
+
it("should be invalid when first rendered", () => {
|
|
753
|
+
// Arrange
|
|
754
|
+
const apiOptions: APIOptions = {
|
|
755
|
+
crossOutEnabled: false,
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// Act
|
|
759
|
+
const {renderer} = renderQuestion(question, apiOptions);
|
|
760
|
+
|
|
761
|
+
// Assert
|
|
762
|
+
expect(renderer).toHaveInvalidInput();
|
|
763
|
+
});
|
|
764
|
+
|
|
559
765
|
it("should be invalid when incorrect number of choices selected", () => {
|
|
560
766
|
// Arrange
|
|
561
767
|
const apiOptions: APIOptions = {
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
phoneMargin,
|
|
15
15
|
negativePhoneMargin,
|
|
16
16
|
} from "../styles/constants.js";
|
|
17
|
+
import a11y from "../util/a11y.js";
|
|
17
18
|
|
|
18
19
|
import GradedGroupWidget from "./graded-group.jsx";
|
|
19
20
|
|
|
@@ -51,7 +52,13 @@ class Indicators extends React.Component<IndicatorsProps> {
|
|
|
51
52
|
styles.selectedIndicator,
|
|
52
53
|
)}
|
|
53
54
|
onClick={() => this.props.onChangeCurrentGroup(i)}
|
|
54
|
-
|
|
55
|
+
>
|
|
56
|
+
{i === this.props.currentGroup && (
|
|
57
|
+
<span className={css(a11y.srOnly)}>
|
|
58
|
+
{i18n._("Current")}
|
|
59
|
+
</span>
|
|
60
|
+
)}
|
|
61
|
+
</div>,
|
|
55
62
|
);
|
|
56
63
|
}
|
|
57
64
|
return <div className={css(styles.indicatorContainer)}>{items}</div>;
|
|
@@ -13,6 +13,7 @@ import * as Changeable from "../mixins/changeable.jsx";
|
|
|
13
13
|
import {ApiOptions} from "../perseus-api.jsx";
|
|
14
14
|
import Renderer from "../renderer.jsx";
|
|
15
15
|
import {
|
|
16
|
+
gray68,
|
|
16
17
|
gray76,
|
|
17
18
|
phoneMargin,
|
|
18
19
|
negativePhoneMargin,
|
|
@@ -294,7 +295,11 @@ class GradedGroup extends React.Component<Props, State> {
|
|
|
294
295
|
{gradeStatus}
|
|
295
296
|
</div>
|
|
296
297
|
)}
|
|
297
|
-
{!apiOptions.isMobile &&
|
|
298
|
+
{!apiOptions.isMobile && (
|
|
299
|
+
<p role="status" aria-live="polite">
|
|
300
|
+
{this.state.message}
|
|
301
|
+
</p>
|
|
302
|
+
)}
|
|
298
303
|
{!apiOptions.isMobile && (
|
|
299
304
|
<Button
|
|
300
305
|
kind="secondary"
|
|
@@ -429,7 +434,7 @@ const styles = StyleSheet.create({
|
|
|
429
434
|
|
|
430
435
|
title: {
|
|
431
436
|
fontSize: 12,
|
|
432
|
-
color:
|
|
437
|
+
color: gray68,
|
|
433
438
|
textTransform: "uppercase",
|
|
434
439
|
marginBottom: 11,
|
|
435
440
|
letterSpacing: 0.8,
|
package/src/widgets/matrix.jsx
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// @flow
|
|
4
4
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
5
5
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
6
|
+
import {StyleSheet} from "aphrodite";
|
|
6
7
|
import classNames from "classnames";
|
|
7
8
|
import * as React from "react";
|
|
8
9
|
import ReactDOM from "react-dom";
|
|
@@ -324,8 +325,23 @@ class Matrix extends React.Component<Props, State> {
|
|
|
324
325
|
<NumberInput {...inputProps} />
|
|
325
326
|
);
|
|
326
327
|
} else {
|
|
328
|
+
const updatedProps = {
|
|
329
|
+
...inputProps,
|
|
330
|
+
style: StyleSheet.create({
|
|
331
|
+
// eslint-disable-next-line react-native/no-unused-styles
|
|
332
|
+
input: {
|
|
333
|
+
...inputProps.style,
|
|
334
|
+
display: "inline-block",
|
|
335
|
+
padding: 0,
|
|
336
|
+
backgroundColor: outside
|
|
337
|
+
? "#f3f3f3"
|
|
338
|
+
: "#fff",
|
|
339
|
+
},
|
|
340
|
+
}).input,
|
|
341
|
+
};
|
|
342
|
+
|
|
327
343
|
MatrixInput = (
|
|
328
|
-
<TextInput {...
|
|
344
|
+
<TextInput {...updatedProps} />
|
|
329
345
|
);
|
|
330
346
|
}
|
|
331
347
|
return (
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import {linterContextDefault} from "@khanacademy/perseus-linter";
|
|
5
5
|
import * as i18n from "@khanacademy/wonder-blocks-i18n";
|
|
6
|
-
import
|
|
6
|
+
import {StyleSheet} from "aphrodite";
|
|
7
7
|
import * as React from "react";
|
|
8
8
|
import _ from "underscore";
|
|
9
9
|
|
|
@@ -419,7 +419,6 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
419
419
|
const rubric = this.props.reviewModeRubric;
|
|
420
420
|
const answers = this.getAnswerBlurb(rubric);
|
|
421
421
|
const answerBlurb = answers[0];
|
|
422
|
-
const classes = [];
|
|
423
422
|
|
|
424
423
|
let labelText = this.props.labelText;
|
|
425
424
|
if (labelText == null || labelText === "") {
|
|
@@ -449,12 +448,20 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
449
448
|
);
|
|
450
449
|
}
|
|
451
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
|
+
|
|
452
460
|
const input = (
|
|
453
461
|
<InputWithExamples
|
|
454
462
|
ref={(ref) => (this.inputRef = ref)}
|
|
455
463
|
value={this.props.currentValue}
|
|
456
464
|
onChange={this.handleChange}
|
|
457
|
-
className={classNames(classes)}
|
|
458
465
|
labelText={labelText}
|
|
459
466
|
type={this._getInputType()}
|
|
460
467
|
examples={this.examples()}
|
|
@@ -463,8 +470,7 @@ export class NumericInput extends React.Component<Props, State> {
|
|
|
463
470
|
onBlur={this._handleBlur}
|
|
464
471
|
id={this.props.widgetId}
|
|
465
472
|
disabled={this.props.apiOptions.readOnly}
|
|
466
|
-
|
|
467
|
-
highlightLint={this.props.highlightLint}
|
|
473
|
+
style={styles.input}
|
|
468
474
|
/>
|
|
469
475
|
);
|
|
470
476
|
|
|
@@ -542,7 +548,7 @@ type RenderProps = {|
|
|
|
542
548
|
name: "integer" | "decimal" | "proper" | "improper" | "mixed" | "pi",
|
|
543
549
|
|}>,
|
|
544
550
|
labelText: string,
|
|
545
|
-
size:
|
|
551
|
+
size: "normal" | "small",
|
|
546
552
|
coefficient: boolean,
|
|
547
553
|
rightAlign?: boolean,
|
|
548
554
|
static: boolean,
|
|
@@ -5,20 +5,31 @@ import * as React from "react";
|
|
|
5
5
|
import _ from "underscore";
|
|
6
6
|
|
|
7
7
|
import * as Changeable from "../mixins/changeable.jsx";
|
|
8
|
-
import
|
|
8
|
+
import {removeDenylistProps} from "../mixins/widget-prop-blacklist.js";
|
|
9
9
|
import PerseusMarkdown from "../perseus-markdown.jsx";
|
|
10
10
|
|
|
11
|
+
import PassageWidgetExport from "./passage.jsx";
|
|
12
|
+
|
|
11
13
|
import type {PerseusPassageRefWidgetOptions} from "../perseus-types.js";
|
|
12
14
|
import type {
|
|
13
15
|
ChangeFn,
|
|
14
16
|
PerseusScore,
|
|
15
17
|
WidgetExports,
|
|
16
18
|
WidgetProps,
|
|
19
|
+
LinterContextProps,
|
|
17
20
|
} from "../types.js";
|
|
21
|
+
import type {Reference} from "./passage.jsx";
|
|
18
22
|
|
|
19
23
|
const EN_DASH = "\u2013";
|
|
20
24
|
|
|
21
|
-
type UserInput =
|
|
25
|
+
type UserInput = {|
|
|
26
|
+
...RenderProps,
|
|
27
|
+
static: ?boolean,
|
|
28
|
+
reviewModeRubric: Rubric,
|
|
29
|
+
linterContext: LinterContextProps,
|
|
30
|
+
isLastUsedWidget: boolean,
|
|
31
|
+
alignment: ?string,
|
|
32
|
+
|};
|
|
22
33
|
|
|
23
34
|
type RenderProps = {|
|
|
24
35
|
passageNumber: PerseusPassageRefWidgetOptions["passageNumber"],
|
|
@@ -64,8 +75,10 @@ class PassageRef extends React.Component<Props, State> {
|
|
|
64
75
|
);
|
|
65
76
|
}
|
|
66
77
|
|
|
78
|
+
// TODO(TB): getUserInput needs to be updated to only return
|
|
79
|
+
// props input by the user. Currently returns all the widget's props.
|
|
67
80
|
getUserInput: () => UserInput = () => {
|
|
68
|
-
return
|
|
81
|
+
return removeDenylistProps(this.props);
|
|
69
82
|
};
|
|
70
83
|
|
|
71
84
|
render(): React.Node {
|
|
@@ -144,11 +157,14 @@ class PassageRef extends React.Component<Props, State> {
|
|
|
144
157
|
};
|
|
145
158
|
|
|
146
159
|
_updateRange: () => void = () => {
|
|
147
|
-
|
|
160
|
+
// Note(TB): findWidgets runs findInternal and findExternal;
|
|
161
|
+
// findExternal runs findInternal for the renderers involved;
|
|
162
|
+
// findInternal returns type $ReadOnlyArray<?Widget>
|
|
163
|
+
const passage: ?PassageWidgetExport.widget = this.props.findWidgets(
|
|
148
164
|
"passage " + this.props.passageNumber,
|
|
149
165
|
)[0];
|
|
150
166
|
|
|
151
|
-
let refInfo = null;
|
|
167
|
+
let refInfo: ?Reference = null;
|
|
152
168
|
if (passage) {
|
|
153
169
|
refInfo = passage.getReference(this.props.referenceNumber);
|
|
154
170
|
}
|
package/src/widgets/passage.jsx
CHANGED
|
@@ -84,6 +84,8 @@ const styles = StyleSheet.create({
|
|
|
84
84
|
width: "100%",
|
|
85
85
|
top: 0,
|
|
86
86
|
left: 0,
|
|
87
|
+
// keep from blocking text selection
|
|
88
|
+
zIndex: -1,
|
|
87
89
|
},
|
|
88
90
|
});
|
|
89
91
|
|
|
@@ -124,7 +126,7 @@ type PassageState = {|
|
|
|
124
126
|
|};
|
|
125
127
|
|
|
126
128
|
// Information about a passage reference, used in inter-widgets.
|
|
127
|
-
type Reference = {
|
|
129
|
+
export type Reference = {
|
|
128
130
|
startLine: number,
|
|
129
131
|
endLine: number,
|
|
130
132
|
content: ?string,
|
|
@@ -68,17 +68,21 @@ export const Interactive = (args: StoryArgs): React.Node => {
|
|
|
68
68
|
return <BaseRadio {...overwrittenProps} />;
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
-
export const
|
|
71
|
+
export const SingleSelectWithNothingSelected = (
|
|
72
|
+
args: StoryArgs,
|
|
73
|
+
): React.Node => {
|
|
72
74
|
const overwrittenProps = {...defaultProps, multipleSelect: false};
|
|
73
75
|
return <BaseRadio {...overwrittenProps} />;
|
|
74
76
|
};
|
|
75
77
|
|
|
76
|
-
export const
|
|
78
|
+
export const MultipleSelectWithNothingSelected = (
|
|
79
|
+
args: StoryArgs,
|
|
80
|
+
): React.Node => {
|
|
77
81
|
const overwrittenProps = {...defaultProps, multipleSelect: true};
|
|
78
82
|
return <BaseRadio {...overwrittenProps} />;
|
|
79
83
|
};
|
|
80
84
|
|
|
81
|
-
export const
|
|
85
|
+
export const MultipleSelectWithCountChoicesLabel = (
|
|
82
86
|
args: StoryArgs,
|
|
83
87
|
): React.Node => {
|
|
84
88
|
const overwrittenProps = {
|
|
@@ -247,7 +247,9 @@ function BaseRadio(props: Props): React.Node {
|
|
|
247
247
|
<fieldset
|
|
248
248
|
className={`perseus-widget-radio-fieldset ${responsiveClassName}`}
|
|
249
249
|
>
|
|
250
|
-
|
|
250
|
+
{shouldShowInstructions && (
|
|
251
|
+
<legend className="perseus-sr-only">{instructions}</legend>
|
|
252
|
+
)}
|
|
251
253
|
{shouldShowInstructions && (
|
|
252
254
|
<div className={instructionsClassName} aria-hidden="true">
|
|
253
255
|
{instructions}
|
|
@@ -26,6 +26,7 @@ function CrossOutLine(props: {color: string, sat?: boolean, ...}): React.Node {
|
|
|
26
26
|
height={crossOutLineSize}
|
|
27
27
|
viewBox={`0 0 ${crossOutLineSize} ${crossOutLineSize}`}
|
|
28
28
|
className={css(styles.crossOutLine)}
|
|
29
|
+
data-test-id="choice-icon__cross-out-line"
|
|
29
30
|
>
|
|
30
31
|
<line
|
|
31
32
|
// The line stretches from the bottom-left to top-right.
|
|
@@ -47,8 +48,8 @@ const styles = StyleSheet.create({
|
|
|
47
48
|
crossOutLine: {
|
|
48
49
|
// Center the icon within the container.
|
|
49
50
|
position: "absolute",
|
|
50
|
-
top: `
|
|
51
|
-
left: `
|
|
51
|
+
top: `0px`,
|
|
52
|
+
left: `0px`,
|
|
52
53
|
},
|
|
53
54
|
});
|
|
54
55
|
|