@khanacademy/perseus 17.6.0 → 17.6.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 +17 -0
- package/dist/es/index.js +402 -606
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +411 -603
- package/dist/index.js.map +1 -1
- package/dist/perseus-types.d.ts +2 -1
- package/dist/strings.js +1 -0
- package/dist/widgets/deprecated-standin/index.d.ts +32 -0
- package/dist/widgets/label-image/answer-choices.d.ts +8 -40
- package/dist/widgets/label-image/answer-pill.d.ts +3 -1
- package/dist/widgets/label-image/marker.d.ts +10 -5
- package/dist/widgets/label-image.d.ts +1 -8
- package/dist/widgets.d.ts +9 -0
- package/package.json +34 -33
- package/src/__tests__/widgets.test.ts +20 -0
- package/src/extra-widgets.ts +2 -0
- package/src/init.ts +5 -0
- package/src/perseus-types.ts +4 -1
- package/src/util/register-all-widgets-for-testing.ts +4 -0
- package/src/widgets/__stories__/orderer.stories.tsx +5 -1
- package/src/widgets/__testdata__/orderer.testdata.ts +66 -0
- package/src/widgets/deprecated-standin/__stories__/deprecated-standin.stories.tsx +34 -0
- package/src/widgets/deprecated-standin/__tests__/__snapshots__/deprecated-standin.test.ts.snap +71 -0
- package/src/widgets/deprecated-standin/__tests__/deprecated-standin.test.ts +93 -0
- package/src/widgets/deprecated-standin/index.tsx +57 -0
- package/src/widgets/label-image/__stories__/answer-choices.stories.tsx +29 -34
- package/src/widgets/label-image/__tests__/answer-pill.test.tsx +2 -10
- package/src/widgets/label-image/answer-choices.tsx +60 -278
- package/src/widgets/label-image/answer-pill.tsx +22 -13
- package/src/widgets/label-image/marker.tsx +103 -146
- package/src/widgets/label-image.test.ts +0 -2
- package/src/widgets/label-image.tsx +94 -195
- package/src/widgets.ts +24 -0
- package/tsconfig-build.tsbuildinfo +1 -1
|
@@ -7,25 +7,32 @@
|
|
|
7
7
|
|
|
8
8
|
import Color from "@khanacademy/wonder-blocks-color";
|
|
9
9
|
import {View, type StyleType} from "@khanacademy/wonder-blocks-core";
|
|
10
|
-
import {StyleSheet
|
|
10
|
+
import {StyleSheet} from "aphrodite";
|
|
11
11
|
import * as React from "react";
|
|
12
12
|
|
|
13
13
|
import Icon from "../../components/icon";
|
|
14
14
|
import {iconCheck, iconChevronDown, iconMinus} from "../../icon-paths";
|
|
15
15
|
|
|
16
|
+
import {AnswerPill} from "./answer-pill";
|
|
17
|
+
|
|
16
18
|
import type {InteractiveMarkerType} from "./types";
|
|
19
|
+
import type {IconType} from "../../components/icon";
|
|
20
|
+
import type {AnalyticsEventHandlerFn} from "@khanacademy/perseus-core";
|
|
21
|
+
import type {CSSProperties} from "aphrodite";
|
|
17
22
|
|
|
18
23
|
type Props = InteractiveMarkerType & {
|
|
19
24
|
// Whether this marker has been selected by user.
|
|
20
25
|
showSelected: boolean;
|
|
21
26
|
// Whether this marker should pulsate to draw user attention.
|
|
22
27
|
showPulsate: boolean;
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
answerSide: "top" | "bottom" | "left" | "right";
|
|
29
|
+
answerStyles?: CSSProperties;
|
|
30
|
+
showAnswer?: boolean;
|
|
31
|
+
analytics?: {
|
|
32
|
+
onAnalyticsEvent: AnalyticsEventHandlerFn;
|
|
33
|
+
};
|
|
28
34
|
focused: boolean;
|
|
35
|
+
hovered: boolean;
|
|
29
36
|
};
|
|
30
37
|
|
|
31
38
|
function shouldReduceMotion(): boolean {
|
|
@@ -33,6 +40,8 @@ function shouldReduceMotion(): boolean {
|
|
|
33
40
|
return !mediaQuery || mediaQuery.matches;
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
const MARKER_SIZE = 24;
|
|
44
|
+
|
|
36
45
|
export default class Marker extends React.Component<Props> {
|
|
37
46
|
// The marker icon element.
|
|
38
47
|
_icon: HTMLElement | null | undefined;
|
|
@@ -43,57 +52,54 @@ export default class Marker extends React.Component<Props> {
|
|
|
43
52
|
selected: [],
|
|
44
53
|
};
|
|
45
54
|
|
|
46
|
-
renderIcon()
|
|
47
|
-
const {selected, showCorrectness, showSelected, showPulsate
|
|
55
|
+
renderIcon() {
|
|
56
|
+
const {selected, showCorrectness, showSelected, showPulsate} =
|
|
48
57
|
this.props;
|
|
49
58
|
|
|
50
59
|
// Only a single marker may be "selected" at a time.
|
|
51
60
|
// `showSelected` is a controlled prop, that may be set to `true` for
|
|
52
61
|
// one marker at a time.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const isSelected = showSelected || focused;
|
|
62
|
+
const isOpen = showSelected;
|
|
63
|
+
|
|
64
|
+
const selectedAnswers = selected;
|
|
57
65
|
|
|
58
|
-
let innerIcon: React.ReactElement | undefined;
|
|
59
66
|
let iconStyles: StyleType;
|
|
60
67
|
|
|
68
|
+
const iconNull: IconType = {
|
|
69
|
+
path: "",
|
|
70
|
+
height: 1,
|
|
71
|
+
width: 1,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// default dot
|
|
75
|
+
let args: Icon["props"] = {
|
|
76
|
+
size: MARKER_SIZE,
|
|
77
|
+
color: Color.white,
|
|
78
|
+
icon: iconNull,
|
|
79
|
+
};
|
|
80
|
+
|
|
61
81
|
if (showCorrectness) {
|
|
62
|
-
innerIcon = (
|
|
63
|
-
<Icon
|
|
64
|
-
icon={showCorrectness === "correct" ? iconCheck : iconMinus}
|
|
65
|
-
size={24}
|
|
66
|
-
color={Color.white}
|
|
67
|
-
/>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
|
-
if (showCorrectness === "correct") {
|
|
71
|
-
iconStyles = [
|
|
72
|
-
styles.markerGraded,
|
|
73
|
-
styles.markerCorrect,
|
|
74
|
-
isSelected && styles.markerSelected,
|
|
75
|
-
];
|
|
76
|
-
} else {
|
|
77
|
-
iconStyles = [
|
|
78
|
-
styles.markerGraded,
|
|
79
|
-
styles.markerIncorrect,
|
|
80
|
-
isSelected && styles.markerSelected,
|
|
81
|
-
];
|
|
82
|
-
}
|
|
83
|
-
} else if (focused) {
|
|
84
|
-
iconStyles = [
|
|
85
|
-
styles.markerFocused,
|
|
86
|
-
selected && selected.length > 0 && styles.markerFilled,
|
|
87
|
-
showSelected && styles.markerSelected,
|
|
88
|
-
];
|
|
89
|
-
} else if (selected && selected.length > 0) {
|
|
90
82
|
iconStyles = [
|
|
91
|
-
styles.
|
|
92
|
-
|
|
83
|
+
styles.markerGraded,
|
|
84
|
+
showCorrectness === "correct"
|
|
85
|
+
? styles.markerCorrect
|
|
86
|
+
: styles.markerIncorrect,
|
|
87
|
+
isOpen && styles.markerSelected,
|
|
93
88
|
];
|
|
94
|
-
|
|
89
|
+
args = {
|
|
90
|
+
...args,
|
|
91
|
+
icon: showCorrectness === "correct" ? iconCheck : iconMinus,
|
|
92
|
+
};
|
|
93
|
+
} else if (selectedAnswers && selectedAnswers.length > 0) {
|
|
94
|
+
iconStyles = [styles.markerFilled, isOpen && styles.markerSelected];
|
|
95
|
+
} else if (isOpen) {
|
|
95
96
|
iconStyles = [styles.markerSelected];
|
|
96
|
-
|
|
97
|
+
args = {
|
|
98
|
+
...args,
|
|
99
|
+
icon: iconChevronDown,
|
|
100
|
+
size: 8,
|
|
101
|
+
};
|
|
102
|
+
} else if (showPulsate) {
|
|
97
103
|
iconStyles = [
|
|
98
104
|
styles.markerPulsateBase,
|
|
99
105
|
shouldReduceMotion()
|
|
@@ -102,90 +108,73 @@ export default class Marker extends React.Component<Props> {
|
|
|
102
108
|
];
|
|
103
109
|
}
|
|
104
110
|
|
|
105
|
-
if (isSelected && selected && selected.length === 0) {
|
|
106
|
-
innerIcon = (
|
|
107
|
-
<Icon icon={iconChevronDown} size={8} color={Color.white} />
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
111
|
return (
|
|
112
112
|
<View
|
|
113
113
|
style={[styles.markerIcon, iconStyles]}
|
|
114
114
|
ref={(node) => (this._icon = node)}
|
|
115
115
|
>
|
|
116
|
-
{
|
|
116
|
+
<Icon {...args} />
|
|
117
117
|
</View>
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
render(): React.ReactNode {
|
|
122
|
-
const {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
const {
|
|
123
|
+
showCorrectness,
|
|
124
|
+
selected,
|
|
125
|
+
showAnswer,
|
|
126
|
+
answerSide,
|
|
127
|
+
answerStyles,
|
|
128
|
+
hovered,
|
|
129
|
+
focused,
|
|
130
|
+
} = this.props;
|
|
131
|
+
|
|
132
|
+
// We cannot make dropdown openers untabbable, so we isolate tabbing
|
|
133
|
+
// to answer pills when interaction is disabled.
|
|
134
|
+
const markerDisabled = showCorrectness === "correct";
|
|
135
|
+
const active = hovered || focused;
|
|
127
136
|
|
|
128
137
|
return (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
138
|
+
<>
|
|
139
|
+
<View
|
|
140
|
+
style={[
|
|
141
|
+
styles.marker,
|
|
142
|
+
active && !markerDisabled && styles.markerActive,
|
|
143
|
+
]}
|
|
144
|
+
>
|
|
145
|
+
{this.renderIcon()}
|
|
146
|
+
</View>
|
|
147
|
+
{!!selected && showAnswer && (
|
|
148
|
+
<AnswerPill
|
|
149
|
+
selectedAnswers={selected}
|
|
150
|
+
showCorrectness={showCorrectness}
|
|
151
|
+
side={answerSide}
|
|
152
|
+
style={answerStyles}
|
|
153
|
+
markerRef={this._icon ?? undefined}
|
|
154
|
+
hovered={hovered}
|
|
155
|
+
focused={focused}
|
|
156
|
+
/>
|
|
135
157
|
)}
|
|
136
|
-
|
|
137
|
-
left: `${x}%`,
|
|
138
|
-
top: `${y}%`,
|
|
139
|
-
}}
|
|
140
|
-
tabIndex={isDisabled ? -1 : 0}
|
|
141
|
-
onFocus={this.props.onFocus}
|
|
142
|
-
onBlur={this.props.onBlur}
|
|
143
|
-
onClick={(e) => {
|
|
144
|
-
e.preventDefault();
|
|
145
|
-
this.props.onClick(e);
|
|
146
|
-
}}
|
|
147
|
-
onKeyDown={(e) => this.props.onKeyDown(e)}
|
|
148
|
-
>
|
|
149
|
-
{this.renderIcon()}
|
|
150
|
-
</button>
|
|
158
|
+
</>
|
|
151
159
|
);
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
|
|
155
|
-
const lightShadowColor = "rgba(33, 36, 44, 0.16)";
|
|
156
|
-
|
|
157
|
-
const markerSize = 20;
|
|
158
|
-
|
|
159
163
|
const styles = StyleSheet.create({
|
|
160
|
-
unstyledButton: {
|
|
161
|
-
padding: 0,
|
|
162
|
-
|
|
163
|
-
overflow: "visible",
|
|
164
|
-
|
|
165
|
-
color: "inherit",
|
|
166
|
-
font: "inherit",
|
|
167
|
-
|
|
168
|
-
lineHeight: "normal",
|
|
169
|
-
|
|
170
|
-
background: "none",
|
|
171
|
-
border: "none",
|
|
172
|
-
},
|
|
173
|
-
|
|
174
164
|
marker: {
|
|
175
165
|
position: "absolute",
|
|
176
|
-
outline: "none",
|
|
177
166
|
|
|
178
|
-
// Center marker position based on it's maximum size.
|
|
179
|
-
width: markerSize,
|
|
180
|
-
height: markerSize,
|
|
181
167
|
backgroundColor: Color.white,
|
|
182
|
-
borderRadius:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
168
|
+
borderRadius: MARKER_SIZE,
|
|
169
|
+
|
|
170
|
+
// Center marker position based on its maximum size.
|
|
171
|
+
width: MARKER_SIZE,
|
|
172
|
+
height: MARKER_SIZE,
|
|
173
|
+
marginLeft: MARKER_SIZE / -2,
|
|
174
|
+
marginTop: MARKER_SIZE / -2,
|
|
186
175
|
|
|
187
|
-
|
|
188
|
-
|
|
176
|
+
// Add a shadow to the marker to make it stand out from the image.
|
|
177
|
+
boxShadow: `0 8px 8px ${Color.offBlack8}`,
|
|
189
178
|
},
|
|
190
179
|
|
|
191
180
|
// The base and unfilled marker style.
|
|
@@ -194,24 +183,13 @@ const styles = StyleSheet.create({
|
|
|
194
183
|
alignItems: "center",
|
|
195
184
|
justifyContent: "center",
|
|
196
185
|
|
|
197
|
-
boxSizing: "
|
|
198
|
-
|
|
199
|
-
width: markerSize,
|
|
200
|
-
height: markerSize,
|
|
186
|
+
boxSizing: "border-box",
|
|
201
187
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
cursor: "pointer",
|
|
188
|
+
width: MARKER_SIZE,
|
|
189
|
+
height: MARKER_SIZE,
|
|
205
190
|
|
|
206
191
|
border: `2px solid ${Color.offBlack64}`,
|
|
207
|
-
borderRadius:
|
|
208
|
-
|
|
209
|
-
boxShadow: `0 8px 8px ${Color.offBlack8}`,
|
|
210
|
-
|
|
211
|
-
":hover": {
|
|
212
|
-
outline: `2px solid ${Color.blue}`,
|
|
213
|
-
outlineOffset: 2,
|
|
214
|
-
},
|
|
192
|
+
borderRadius: MARKER_SIZE,
|
|
215
193
|
},
|
|
216
194
|
|
|
217
195
|
// The animation that presents the marker to the learner
|
|
@@ -239,10 +217,6 @@ const styles = StyleSheet.create({
|
|
|
239
217
|
|
|
240
218
|
markerUnfilledPulsateInfinite: {
|
|
241
219
|
animationIterationCount: "infinite",
|
|
242
|
-
|
|
243
|
-
":hover": {
|
|
244
|
-
outline: `0px solid ${Color.blue}`,
|
|
245
|
-
},
|
|
246
220
|
},
|
|
247
221
|
|
|
248
222
|
markerUnfilledPulsateOnce: {
|
|
@@ -250,7 +224,7 @@ const styles = StyleSheet.create({
|
|
|
250
224
|
animationIterationCount: "2",
|
|
251
225
|
},
|
|
252
226
|
|
|
253
|
-
|
|
227
|
+
markerActive: {
|
|
254
228
|
outline: `2px solid ${Color.blue}`,
|
|
255
229
|
outlineOffset: 2,
|
|
256
230
|
},
|
|
@@ -261,40 +235,23 @@ const styles = StyleSheet.create({
|
|
|
261
235
|
|
|
262
236
|
border: `solid 4px ${Color.white}`,
|
|
263
237
|
backgroundColor: Color.blue,
|
|
264
|
-
borderRadius:
|
|
238
|
+
borderRadius: MARKER_SIZE,
|
|
265
239
|
transform: "rotate(180deg)",
|
|
266
|
-
|
|
267
|
-
":hover": {
|
|
268
|
-
outline: `2px solid ${Color.blue}`,
|
|
269
|
-
outlineOffset: -2,
|
|
270
|
-
},
|
|
271
240
|
},
|
|
272
241
|
|
|
273
242
|
// The learner has made a selection
|
|
274
243
|
markerFilled: {
|
|
275
|
-
width: markerSize - 2,
|
|
276
|
-
height: markerSize - 2,
|
|
277
|
-
borderRadius: markerSize - 2,
|
|
278
244
|
backgroundColor: "#ECF3FE",
|
|
279
|
-
|
|
280
|
-
marginLeft: 3,
|
|
281
|
-
marginTop: 0,
|
|
282
|
-
|
|
283
245
|
border: `4px solid ${Color.blue}`,
|
|
284
|
-
|
|
285
|
-
boxShadow: `0 1px 1px 0 ${lightShadowColor}`,
|
|
286
246
|
},
|
|
287
247
|
|
|
288
248
|
markerGraded: {
|
|
289
|
-
width:
|
|
290
|
-
height:
|
|
291
|
-
marginLeft: 1,
|
|
292
|
-
marginTop: 1,
|
|
249
|
+
width: MARKER_SIZE,
|
|
250
|
+
height: MARKER_SIZE,
|
|
293
251
|
|
|
294
252
|
justifyContent: "center",
|
|
295
253
|
alignItems: "center",
|
|
296
254
|
border: `2px solid ${Color.white}`,
|
|
297
|
-
boxShadow: `0 8px 8px ${Color.offBlack8}`,
|
|
298
255
|
},
|
|
299
256
|
|
|
300
257
|
markerCorrect: {
|
|
@@ -912,8 +912,6 @@ describe("LabelImage", function () {
|
|
|
912
912
|
);
|
|
913
913
|
userEvent.click(markerButton);
|
|
914
914
|
|
|
915
|
-
await screen.findByRole("dialog");
|
|
916
|
-
|
|
917
915
|
// Select a choice
|
|
918
916
|
const choices = await screen.findAllByText("Trucks");
|
|
919
917
|
const choice = choices[choices.length - 1];
|