@khanacademy/perseus 1.1.1 → 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 +8 -0
- package/dist/es/index.css +1 -0
- package/dist/es/index.js +15254 -15218
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +1 -0
- package/dist/index.js +15270 -15233
- 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/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/passage.less +1 -0
- package/src/types.js +1 -1
- package/src/widgets/__tests__/__snapshots__/graded-group-set-jipt_test.jsx.snap +3 -3
- package/src/widgets/__tests__/__snapshots__/graded-group-set_test.jsx.snap +7 -1
- package/src/widgets/__tests__/__snapshots__/graded-group_test.jsx.snap +2 -2
- package/src/widgets/__tests__/__snapshots__/group_test.jsx.snap +30 -10
- 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 +2 -1
- 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,
|
|
@@ -433,7 +434,7 @@ const styles = StyleSheet.create({
|
|
|
433
434
|
|
|
434
435
|
title: {
|
|
435
436
|
fontSize: 12,
|
|
436
|
-
color:
|
|
437
|
+
color: gray68,
|
|
437
438
|
textTransform: "uppercase",
|
|
438
439
|
marginBottom: 11,
|
|
439
440
|
letterSpacing: 0.8,
|
|
@@ -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
|
|
|
@@ -153,11 +153,13 @@ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
|
|
|
153
153
|
// radio icon
|
|
154
154
|
data-is-radio-icon={true}
|
|
155
155
|
>
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
156
|
+
<div className={css(styles.innerWrapper)}>
|
|
157
|
+
<ChoiceInner
|
|
158
|
+
letter={letter}
|
|
159
|
+
showCorrectness={showCorrectness}
|
|
160
|
+
correct={correct}
|
|
161
|
+
/>
|
|
162
|
+
</div>
|
|
161
163
|
</div>
|
|
162
164
|
</FocusRing>
|
|
163
165
|
{crossedOut && <CrossOutLine color={dynamicStyles.borderColor} />}
|
|
@@ -166,6 +168,15 @@ function LibraryChoiceIcon(props: LibraryChoiceIconProps): React.Node {
|
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
const styles = StyleSheet.create({
|
|
171
|
+
innerWrapper: {
|
|
172
|
+
width: LIBRARY_ICON_SIZE,
|
|
173
|
+
height: LIBRARY_ICON_SIZE,
|
|
174
|
+
|
|
175
|
+
// Center the contents of the icon.
|
|
176
|
+
display: "flex",
|
|
177
|
+
alignItems: "center",
|
|
178
|
+
justifyContent: "center",
|
|
179
|
+
},
|
|
169
180
|
circle: {
|
|
170
181
|
// Make the circle
|
|
171
182
|
width: LIBRARY_ICON_SIZE,
|
|
@@ -182,7 +193,7 @@ const styles = StyleSheet.create({
|
|
|
182
193
|
fontWeight: "bold",
|
|
183
194
|
fontSize: 12,
|
|
184
195
|
|
|
185
|
-
// Center the
|
|
196
|
+
// Center the icon wrapper.
|
|
186
197
|
display: "flex",
|
|
187
198
|
alignItems: "center",
|
|
188
199
|
justifyContent: "center",
|
|
@@ -241,6 +241,7 @@ function Choice(props: ChoicePropsWithForwardRef): React.Node {
|
|
|
241
241
|
alignContent: "center",
|
|
242
242
|
paddingTop: Spacing.xSmall_8,
|
|
243
243
|
paddingBottom: Spacing.xSmall_8,
|
|
244
|
+
paddingLeft: Spacing.xSmall_8,
|
|
244
245
|
}}
|
|
245
246
|
>
|
|
246
247
|
<ChoiceIcon
|
|
@@ -412,7 +413,7 @@ const styles = StyleSheet.create({
|
|
|
412
413
|
nonSatRationale: {
|
|
413
414
|
padding: intermediateCheckboxPadding,
|
|
414
415
|
paddingTop: 0,
|
|
415
|
-
marginLeft:
|
|
416
|
+
marginLeft: 54,
|
|
416
417
|
[mediaQueries.smOrSmaller]: {
|
|
417
418
|
padding: intermediateCheckboxPaddingPhone,
|
|
418
419
|
paddingTop: 0,
|