@khanacademy/perseus 17.0.0 → 17.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.
- package/CHANGELOG.md +59 -0
- package/dist/dependencies.d.ts +1 -0
- package/dist/es/index.css +20 -0
- package/dist/es/index.js +443 -338
- package/dist/es/index.js.map +1 -1
- package/dist/index.css +20 -0
- package/dist/index.js +444 -340
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +1 -0
- package/dist/strings.js +1 -0
- package/dist/widgets/label-image/answer-pill.d.ts +10 -0
- package/dist/widgets/label-image/hide-answers-toggle.d.ts +6 -0
- package/dist/widgets/label-image/marker.d.ts +6 -9
- package/dist/widgets/label-image/types.d.ts +1 -0
- package/dist/widgets/label-image.d.ts +101 -12
- package/package.json +30 -22
- package/src/__tests__/__snapshots__/renderer.test.tsx.snap +3 -3
- package/src/dependencies.ts +4 -0
- package/src/perseus-types.ts +2 -0
- package/src/styles/widgets/label-image.less +32 -0
- package/src/widgets/__stories__/label-image.stories.tsx +73 -3
- package/src/widgets/__testdata__/label-image.testdata.ts +118 -1
- package/src/widgets/__tests__/__snapshots__/dropdown.test.ts.snap +2 -2
- package/src/widgets/label-image/__stories__/answer-pill.stories.tsx +62 -0
- package/src/widgets/label-image/__tests__/answer-pill.test.tsx +83 -0
- package/src/widgets/label-image/__tests__/hide-answers-toggle.test.tsx +39 -0
- package/src/widgets/label-image/answer-choices.tsx +7 -1
- package/src/widgets/label-image/answer-pill.tsx +89 -0
- package/src/widgets/label-image/hide-answers-toggle.tsx +40 -0
- package/src/widgets/label-image/marker.tsx +116 -174
- package/src/widgets/label-image/types.ts +1 -0
- package/src/widgets/label-image.test.ts +142 -52
- package/src/widgets/label-image.tsx +212 -135
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -15,19 +15,32 @@ import * as ReactDOM from "react-dom";
|
|
|
15
15
|
|
|
16
16
|
import AssetContext from "../asset-context";
|
|
17
17
|
import SvgImage from "../components/svg-image";
|
|
18
|
+
import {useDependencies} from "../dependencies";
|
|
18
19
|
import Renderer from "../renderer";
|
|
19
20
|
import {typography} from "../styles/global-styles";
|
|
20
21
|
import mediaQueries from "../styles/media-queries";
|
|
21
22
|
|
|
22
23
|
import AnswerChoices from "./label-image/answer-choices";
|
|
24
|
+
import {AnswerPill} from "./label-image/answer-pill";
|
|
25
|
+
import {HideAnswersToggle} from "./label-image/hide-answers-toggle";
|
|
23
26
|
import Marker from "./label-image/marker";
|
|
24
27
|
|
|
25
28
|
import type {
|
|
26
29
|
InteractiveMarkerType,
|
|
27
30
|
InteractiveMarkerScore,
|
|
28
31
|
} from "./label-image/types";
|
|
32
|
+
import type {DependencyProps} from "../dependencies";
|
|
29
33
|
import type {ChangeableProps} from "../mixins/changeable";
|
|
30
34
|
import type {APIOptions, PerseusScore, WidgetExports} from "../types";
|
|
35
|
+
import type {PropsFor} from "@khanacademy/wonder-blocks-core";
|
|
36
|
+
import type {CSSProperties} from "aphrodite";
|
|
37
|
+
|
|
38
|
+
export type PreferredPopoverDirection =
|
|
39
|
+
| "NONE"
|
|
40
|
+
| "UP"
|
|
41
|
+
| "DOWN"
|
|
42
|
+
| "LEFT"
|
|
43
|
+
| "RIGHT";
|
|
31
44
|
|
|
32
45
|
type MarkersState = {
|
|
33
46
|
markers: ReadonlyArray<InteractiveMarkerType>;
|
|
@@ -56,33 +69,43 @@ type Point = {
|
|
|
56
69
|
y: number;
|
|
57
70
|
};
|
|
58
71
|
|
|
59
|
-
type LabelImageProps = ChangeableProps &
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
type LabelImageProps = ChangeableProps &
|
|
73
|
+
DependencyProps & {
|
|
74
|
+
apiOptions: APIOptions;
|
|
75
|
+
// The list of possible answer choices.
|
|
76
|
+
choices: ReadonlyArray<string>;
|
|
77
|
+
// The question image properties.
|
|
78
|
+
imageAlt: string;
|
|
79
|
+
imageUrl: string;
|
|
80
|
+
imageWidth: number;
|
|
81
|
+
imageHeight: number;
|
|
82
|
+
// The list of label markers on the question image.
|
|
83
|
+
markers: ReadonlyArray<InteractiveMarkerType>;
|
|
84
|
+
// Whether multiple answer choices may be selected for markers.
|
|
85
|
+
multipleAnswers: boolean;
|
|
86
|
+
// Whether to hide answer choices from user instructions.
|
|
87
|
+
hideChoicesFromInstructions: boolean;
|
|
88
|
+
// Whether the question has been answered by the user.
|
|
89
|
+
questionCompleted: boolean;
|
|
90
|
+
// preferred placement for popover (preference, not MUST)
|
|
91
|
+
preferredPopoverDirection?: PreferredPopoverDirection;
|
|
92
|
+
};
|
|
77
93
|
|
|
78
94
|
type LabelImageState = {
|
|
79
95
|
// The user selected marker index, defaults to -1, no selection.
|
|
80
|
-
|
|
96
|
+
activeMarkerIndex: number;
|
|
81
97
|
// Whether any of the markers were interacted with by the user.
|
|
82
98
|
markersInteracted: boolean;
|
|
99
|
+
// The currently focused marker index; defaults to -1, no focus.
|
|
100
|
+
focusedMarkerIndex: number;
|
|
101
|
+
// Hide answer pills.
|
|
102
|
+
hideAnswers: boolean;
|
|
83
103
|
};
|
|
84
104
|
|
|
85
|
-
class LabelImage extends React.Component<
|
|
105
|
+
export class LabelImage extends React.Component<
|
|
106
|
+
LabelImageProps,
|
|
107
|
+
LabelImageState
|
|
108
|
+
> {
|
|
86
109
|
// The rendered markers on the question image for labeling.
|
|
87
110
|
_markers: Array<Marker | null | undefined>;
|
|
88
111
|
|
|
@@ -182,7 +205,21 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
182
205
|
static imageSideForMarkerPosition(
|
|
183
206
|
x: number,
|
|
184
207
|
y: number,
|
|
208
|
+
preferredDirection: PreferredPopoverDirection | undefined,
|
|
185
209
|
): "bottom" | "left" | "right" | "top" | "center" {
|
|
210
|
+
// Special handling for preferred size
|
|
211
|
+
if (preferredDirection && preferredDirection !== "NONE") {
|
|
212
|
+
if (preferredDirection === "LEFT" && x > 20) {
|
|
213
|
+
return "right";
|
|
214
|
+
} else if (preferredDirection === "RIGHT" && x < 80) {
|
|
215
|
+
return "left";
|
|
216
|
+
} else if (preferredDirection === "UP" && y > 20) {
|
|
217
|
+
return "bottom";
|
|
218
|
+
} else if (preferredDirection === "DOWN" && y < 80) {
|
|
219
|
+
return "top";
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
186
223
|
// Special handling for when marker is positioned near the horizontal
|
|
187
224
|
// edges of the image. We want to ensure the returned side would not
|
|
188
225
|
// result in a popup rendering that may overflow outside the page.
|
|
@@ -337,25 +374,13 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
337
374
|
this._markers = [];
|
|
338
375
|
|
|
339
376
|
this.state = {
|
|
340
|
-
|
|
377
|
+
activeMarkerIndex: -1,
|
|
378
|
+
focusedMarkerIndex: -1,
|
|
341
379
|
markersInteracted: false,
|
|
380
|
+
hideAnswers: false,
|
|
342
381
|
};
|
|
343
382
|
}
|
|
344
383
|
|
|
345
|
-
componentDidMount() {
|
|
346
|
-
document.addEventListener("click", this.handleDocumentClick, true);
|
|
347
|
-
document.addEventListener("keydown", this.handleDocumentKeyDown, true);
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
componentWillUnmount() {
|
|
351
|
-
document.removeEventListener("click", this.handleDocumentClick, true);
|
|
352
|
-
document.removeEventListener(
|
|
353
|
-
"keydown",
|
|
354
|
-
this.handleDocumentKeyDown,
|
|
355
|
-
true,
|
|
356
|
-
);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
384
|
simpleValidate(rubric: LabelImageProps): PerseusScore {
|
|
360
385
|
return LabelImage.validate(this.getUserInput(), rubric);
|
|
361
386
|
}
|
|
@@ -390,15 +415,15 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
390
415
|
}
|
|
391
416
|
|
|
392
417
|
dismissMarkerPopup() {
|
|
393
|
-
const {
|
|
418
|
+
const {activeMarkerIndex} = this.state;
|
|
394
419
|
|
|
395
420
|
// No popup should be open if there's no selected marker.
|
|
396
|
-
if (
|
|
421
|
+
if (activeMarkerIndex === -1) {
|
|
397
422
|
return;
|
|
398
423
|
}
|
|
399
424
|
|
|
400
|
-
this.setState({
|
|
401
|
-
const marker = this._markers[
|
|
425
|
+
this.setState({activeMarkerIndex: -1}, () => {
|
|
426
|
+
const marker = this._markers[activeMarkerIndex];
|
|
402
427
|
// Set focus on the just-deselected-marker, to enable to resume
|
|
403
428
|
// navigating between the markers using the keyboard.
|
|
404
429
|
if (marker) {
|
|
@@ -407,48 +432,6 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
407
432
|
});
|
|
408
433
|
}
|
|
409
434
|
|
|
410
|
-
handleDocumentClick: (e: MouseEvent) => void = (e: MouseEvent) => {
|
|
411
|
-
// Dismiss open popup with answer choices if user clicks outside it.
|
|
412
|
-
if (this._selectedMarkerPopup && this._answerChoices) {
|
|
413
|
-
const answerChoices = ReactDOM.findDOMNode(this._answerChoices);
|
|
414
|
-
|
|
415
|
-
const targetElement = e.target as Node;
|
|
416
|
-
// HACK(michaelpolyak): We want to determine if the click target is
|
|
417
|
-
// contained within the popup layer. As there's no public interface
|
|
418
|
-
// to get this layer from the popup, we traverse several levels of
|
|
419
|
-
// answer choices parents to test whether the click target is
|
|
420
|
-
// contained within.
|
|
421
|
-
const containsEventTarget = (
|
|
422
|
-
element: Element | null | undefined | Text,
|
|
423
|
-
depth = 3,
|
|
424
|
-
) =>
|
|
425
|
-
element &&
|
|
426
|
-
(element.contains(targetElement) ||
|
|
427
|
-
(depth > 0 &&
|
|
428
|
-
containsEventTarget(element.parentElement, depth - 1)));
|
|
429
|
-
|
|
430
|
-
if (!containsEventTarget(answerChoices)) {
|
|
431
|
-
// Close popup and set focus back to the marker for which it was
|
|
432
|
-
// open.
|
|
433
|
-
this.dismissMarkerPopup();
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
};
|
|
437
|
-
|
|
438
|
-
handleDocumentKeyDown: (e: KeyboardEvent) => void = (e: KeyboardEvent) => {
|
|
439
|
-
// Dismiss open popup with answer choices if user presses Escape key.
|
|
440
|
-
if (this._selectedMarkerPopup && e.keyCode === 27) {
|
|
441
|
-
// Ensure other listeners are not triggered on key down event that
|
|
442
|
-
// closes the popup, as this will also dismiss the modal that the
|
|
443
|
-
// widget may be rendered within.
|
|
444
|
-
e.stopPropagation();
|
|
445
|
-
|
|
446
|
-
// Close popup and set focus back to the marker for which it was
|
|
447
|
-
// open.
|
|
448
|
-
this.dismissMarkerPopup();
|
|
449
|
-
}
|
|
450
|
-
};
|
|
451
|
-
|
|
452
435
|
handleMarkerChange(index: number, marker: InteractiveMarkerType) {
|
|
453
436
|
const {markers, onChange} = this.props;
|
|
454
437
|
|
|
@@ -468,16 +451,14 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
468
451
|
onChange({markers: updatedMarkers});
|
|
469
452
|
}
|
|
470
453
|
|
|
471
|
-
|
|
472
|
-
const {
|
|
473
|
-
|
|
474
|
-
e.preventDefault();
|
|
454
|
+
activateMarker(index: number) {
|
|
455
|
+
const {activeMarkerIndex} = this.state;
|
|
475
456
|
|
|
476
457
|
// Select the marker, revealing answer choices.
|
|
477
|
-
if (
|
|
458
|
+
if (activeMarkerIndex !== index) {
|
|
478
459
|
this.setState(
|
|
479
460
|
{
|
|
480
|
-
|
|
461
|
+
activeMarkerIndex: index,
|
|
481
462
|
markersInteracted: true,
|
|
482
463
|
},
|
|
483
464
|
() => {
|
|
@@ -500,7 +481,7 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
500
481
|
}
|
|
501
482
|
}
|
|
502
483
|
|
|
503
|
-
handleMarkerKeyDown(index: number, e: KeyboardEvent) {
|
|
484
|
+
handleMarkerKeyDown(index: number, e: React.KeyboardEvent) {
|
|
504
485
|
const {markers} = this.props;
|
|
505
486
|
|
|
506
487
|
// One is the loneliest number.
|
|
@@ -545,11 +526,11 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
545
526
|
const {choices, markers, multipleAnswers} = this.props;
|
|
546
527
|
|
|
547
528
|
// Compile the user selected answer choices.
|
|
548
|
-
const selected = choices.filter((
|
|
529
|
+
const selected = choices.filter((_, index) => selection[index]);
|
|
549
530
|
|
|
550
531
|
this.handleMarkerChange(index, {
|
|
551
532
|
...markers[index],
|
|
552
|
-
selected,
|
|
533
|
+
selected: selected.length ? selected : undefined,
|
|
553
534
|
});
|
|
554
535
|
|
|
555
536
|
if (!multipleAnswers) {
|
|
@@ -579,12 +560,16 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
579
560
|
checked: selected ? selected.includes(choice) : false,
|
|
580
561
|
}))}
|
|
581
562
|
multipleSelect={multipleAnswers}
|
|
582
|
-
onChange={(selection) =>
|
|
563
|
+
onChange={(selection) => {
|
|
564
|
+
this.props.analytics?.onAnalyticsEvent({
|
|
565
|
+
type: "perseus:label-image:choiced-interacted-with",
|
|
566
|
+
payload: null,
|
|
567
|
+
});
|
|
583
568
|
this.handleAnswerChoicesChangeForMarker(
|
|
584
569
|
index,
|
|
585
570
|
selection,
|
|
586
|
-
)
|
|
587
|
-
}
|
|
571
|
+
);
|
|
572
|
+
}}
|
|
588
573
|
ref={(node) => (this._answerChoices = node)}
|
|
589
574
|
/>
|
|
590
575
|
</div>
|
|
@@ -592,51 +577,75 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
592
577
|
}
|
|
593
578
|
|
|
594
579
|
renderMarkers(): ReadonlyArray<React.ReactNode> {
|
|
595
|
-
const {markers, questionCompleted} =
|
|
580
|
+
const {markers, questionCompleted, preferredPopoverDirection} =
|
|
581
|
+
this.props;
|
|
596
582
|
|
|
597
|
-
const {
|
|
583
|
+
const {activeMarkerIndex, markersInteracted} = this.state;
|
|
598
584
|
|
|
599
585
|
// Render all markers for widget.
|
|
600
586
|
return markers.map((marker, index): React.ReactElement => {
|
|
601
587
|
const score = LabelImage.gradeMarker(marker);
|
|
588
|
+
// Once the question is answered, show markers
|
|
589
|
+
// with correct answers, otherwise passthrough
|
|
590
|
+
// the correctness state.
|
|
591
|
+
const showCorrectness =
|
|
592
|
+
questionCompleted && score.hasAnswers && score.isCorrect
|
|
593
|
+
? "correct"
|
|
594
|
+
: marker.showCorrectness;
|
|
602
595
|
|
|
603
596
|
const element = (
|
|
604
597
|
<Marker
|
|
605
598
|
{...marker}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
// the correctness state.
|
|
609
|
-
showCorrectness={
|
|
610
|
-
questionCompleted && score.hasAnswers && score.isCorrect
|
|
611
|
-
? "correct"
|
|
612
|
-
: marker.showCorrectness
|
|
613
|
-
}
|
|
614
|
-
showSelected={index === selectedMarkerIndex}
|
|
599
|
+
showCorrectness={showCorrectness}
|
|
600
|
+
showSelected={index === activeMarkerIndex}
|
|
615
601
|
showPulsate={!markersInteracted}
|
|
616
602
|
key={`${marker.x}.${marker.y}`}
|
|
617
|
-
onClick={(
|
|
603
|
+
onClick={() => {
|
|
604
|
+
this.props.analytics?.onAnalyticsEvent({
|
|
605
|
+
type: "perseus:label-image:marker-interacted-with",
|
|
606
|
+
payload: null,
|
|
607
|
+
});
|
|
608
|
+
this.activateMarker(index);
|
|
609
|
+
}}
|
|
618
610
|
onKeyDown={(e) => this.handleMarkerKeyDown(index, e)}
|
|
619
611
|
ref={(node) => (this._markers[index] = node)}
|
|
612
|
+
focused={index === this.state.focusedMarkerIndex}
|
|
613
|
+
onFocus={() => {
|
|
614
|
+
this.setState({focusedMarkerIndex: index});
|
|
615
|
+
}}
|
|
616
|
+
onBlur={() => {
|
|
617
|
+
if (index === this.state.focusedMarkerIndex) {
|
|
618
|
+
this.setState({focusedMarkerIndex: -1});
|
|
619
|
+
}
|
|
620
|
+
}}
|
|
620
621
|
/>
|
|
621
622
|
);
|
|
622
623
|
|
|
623
|
-
// The user selected marker is wrapped with a popup that shows its
|
|
624
|
-
// answer choices, otherwise it's returned as is.
|
|
625
|
-
if (index !== selectedMarkerIndex) {
|
|
626
|
-
return element;
|
|
627
|
-
}
|
|
628
|
-
|
|
629
624
|
// Determine whether page is rendered in a narrow browser window.
|
|
630
625
|
const isNarrowPage = window.matchMedia(
|
|
631
626
|
mediaQueries.xsOrSmaller.replace("@media ", ""),
|
|
632
627
|
).matches;
|
|
633
628
|
|
|
634
|
-
|
|
629
|
+
// Determine whether the image is wider than it is tall.
|
|
630
|
+
const isWideImage =
|
|
631
|
+
this.props.imageWidth / 2 > this.props.imageHeight;
|
|
632
|
+
|
|
633
|
+
let side: "bottom" | "left" | "right" | "top";
|
|
634
|
+
let markerPosition;
|
|
635
635
|
// Position popup closest to the center, preferring it renders
|
|
636
636
|
// entirely within the image area.
|
|
637
|
-
if (isNarrowPage) {
|
|
637
|
+
if (isNarrowPage || isWideImage) {
|
|
638
638
|
side = marker.y > 50 ? "top" : "bottom";
|
|
639
|
+
markerPosition = marker.y > 50 ? "bottom" : "top";
|
|
639
640
|
} else {
|
|
641
|
+
markerPosition = LabelImage.imageSideForMarkerPosition(
|
|
642
|
+
marker.x,
|
|
643
|
+
marker.y,
|
|
644
|
+
preferredPopoverDirection,
|
|
645
|
+
);
|
|
646
|
+
if (markerPosition === "center") {
|
|
647
|
+
markerPosition = "bottom";
|
|
648
|
+
}
|
|
640
649
|
// This mirrors the calculated side of where the marker is
|
|
641
650
|
// located within the image, so that popup appears closer to
|
|
642
651
|
// the image center.
|
|
@@ -645,28 +654,69 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
645
654
|
top: "bottom",
|
|
646
655
|
right: "left",
|
|
647
656
|
bottom: "top",
|
|
648
|
-
|
|
649
|
-
}[LabelImage.imageSideForMarkerPosition(marker.x, marker.y)];
|
|
657
|
+
}[markerPosition];
|
|
650
658
|
}
|
|
651
659
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
660
|
+
const answerChoicesActive = index === activeMarkerIndex;
|
|
661
|
+
|
|
662
|
+
const adjustPopoverDistance: CSSProperties = {
|
|
663
|
+
[side]: 15, // move the popover closer to the marker
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const adjustPillDistance: CSSProperties = {
|
|
667
|
+
[`margin${
|
|
668
|
+
markerPosition.charAt(0).toUpperCase() +
|
|
669
|
+
markerPosition.slice(1)
|
|
670
|
+
}`]: 5, // move pill further from marker
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const showAnswerChoice =
|
|
674
|
+
!answerChoicesActive && !this.state.hideAnswers;
|
|
675
|
+
|
|
656
676
|
return (
|
|
657
|
-
<
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
677
|
+
<div key={`answers-${marker.x}.${marker.y}`}>
|
|
678
|
+
<Popover
|
|
679
|
+
content={() => (
|
|
680
|
+
<PopoverContentCore
|
|
681
|
+
style={[
|
|
682
|
+
adjustPopoverDistance,
|
|
683
|
+
styles.choicesPopover,
|
|
684
|
+
]}
|
|
685
|
+
>
|
|
686
|
+
{this.renderAnswerChoicesForMarker(
|
|
687
|
+
index,
|
|
688
|
+
marker,
|
|
689
|
+
)}
|
|
690
|
+
</PopoverContentCore>
|
|
691
|
+
)}
|
|
692
|
+
placement={side}
|
|
693
|
+
onClose={() => this.dismissMarkerPopup()}
|
|
694
|
+
opened={answerChoicesActive}
|
|
695
|
+
ref={(node) => (this._selectedMarkerPopup = node)}
|
|
696
|
+
showTail={false}
|
|
697
|
+
dismissEnabled
|
|
698
|
+
>
|
|
699
|
+
{element}
|
|
700
|
+
</Popover>
|
|
701
|
+
{!!marker.selected && showAnswerChoice && (
|
|
702
|
+
<AnswerPill
|
|
703
|
+
selectedAnswers={marker.selected}
|
|
704
|
+
showCorrectness={showCorrectness}
|
|
705
|
+
markerRef={
|
|
706
|
+
// this throws a warning because it's inadvisable to
|
|
707
|
+
// call ReactDOM fns in render. we aren't storing
|
|
708
|
+
// actual refs in this array, so not much I can do
|
|
709
|
+
// outside of a refactor.
|
|
710
|
+
ReactDOM.findDOMNode(
|
|
711
|
+
this._markers[index],
|
|
712
|
+
) as HTMLElement
|
|
713
|
+
}
|
|
714
|
+
side={side}
|
|
715
|
+
onClick={() => this.activateMarker(index)}
|
|
716
|
+
style={adjustPillDistance}
|
|
717
|
+
/>
|
|
662
718
|
)}
|
|
663
|
-
|
|
664
|
-
opened={true}
|
|
665
|
-
key={`${marker.x}.${marker.y}`}
|
|
666
|
-
ref={(node) => (this._selectedMarkerPopup = node)}
|
|
667
|
-
>
|
|
668
|
-
{element}
|
|
669
|
-
</Popover>
|
|
719
|
+
</div>
|
|
670
720
|
);
|
|
671
721
|
});
|
|
672
722
|
}
|
|
@@ -723,7 +773,7 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
723
773
|
render(): React.ReactNode {
|
|
724
774
|
const {imageAlt, imageUrl, imageWidth, imageHeight} = this.props;
|
|
725
775
|
|
|
726
|
-
const {
|
|
776
|
+
const {activeMarkerIndex} = this.state;
|
|
727
777
|
|
|
728
778
|
return (
|
|
729
779
|
<div>
|
|
@@ -744,7 +794,7 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
744
794
|
// dismiss the popup. If the image is larger in size
|
|
745
795
|
// than its rendered in the widget, this would
|
|
746
796
|
// result in a zoom of the image.
|
|
747
|
-
|
|
797
|
+
activeMarkerIndex !== -1 &&
|
|
748
798
|
styles.imageInteractionDisabled,
|
|
749
799
|
)}
|
|
750
800
|
>
|
|
@@ -762,6 +812,16 @@ class LabelImage extends React.Component<LabelImageProps, LabelImageState> {
|
|
|
762
812
|
</div>
|
|
763
813
|
{this.renderMarkers()}
|
|
764
814
|
</div>
|
|
815
|
+
<HideAnswersToggle
|
|
816
|
+
areAnswersHidden={this.state.hideAnswers}
|
|
817
|
+
onChange={(hideAnswers) => {
|
|
818
|
+
this.props.analytics?.onAnalyticsEvent({
|
|
819
|
+
type: "perseus:label-image:toggle-answers-hidden",
|
|
820
|
+
payload: null,
|
|
821
|
+
});
|
|
822
|
+
this.setState({hideAnswers});
|
|
823
|
+
}}
|
|
824
|
+
/>
|
|
765
825
|
</div>
|
|
766
826
|
);
|
|
767
827
|
}
|
|
@@ -840,10 +900,27 @@ const styles = StyleSheet.create({
|
|
|
840
900
|
},
|
|
841
901
|
});
|
|
842
902
|
|
|
903
|
+
const LabelImageWithDependencies = React.forwardRef<
|
|
904
|
+
LabelImage,
|
|
905
|
+
Omit<PropsFor<typeof LabelImage>, keyof ReturnType<typeof useDependencies>>
|
|
906
|
+
>((props, ref) => {
|
|
907
|
+
const deps = useDependencies();
|
|
908
|
+
return <LabelImage ref={ref} analytics={deps.analytics} {...props} />;
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
// HACK: Propogate "static" methods onto our wrapper component.
|
|
912
|
+
// In the future we should adjust client apps to not depend on these static
|
|
913
|
+
// methods and instead adjust Peresus to provide these facilities through
|
|
914
|
+
// instance methods on our Renderers.
|
|
915
|
+
// @ts-expect-error - TS2339 - Property 'validate' does not exist on type
|
|
916
|
+
LabelImageWithDependencies.validate = LabelImage.validate;
|
|
917
|
+
// @ts-expect-error - TS2339 - Property 'gradeMarker' does not exist on type
|
|
918
|
+
LabelImageWithDependencies.gradeMarker = LabelImage.gradeMarker;
|
|
919
|
+
|
|
843
920
|
export default {
|
|
844
921
|
name: "label-image",
|
|
845
922
|
displayName: "Label Image",
|
|
846
|
-
widget:
|
|
923
|
+
widget: LabelImageWithDependencies,
|
|
847
924
|
accessible: true,
|
|
848
925
|
isLintable: true,
|
|
849
|
-
} as WidgetExports<typeof
|
|
926
|
+
} as WidgetExports<typeof LabelImageWithDependencies>;
|