@lessonkit/react 1.3.0 → 1.4.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.
@@ -0,0 +1,252 @@
1
+ import {
2
+ AssessmentLessonGuard,
3
+ buildAssessmentHandle,
4
+ isDevEnvironment,
5
+ normalizeComponentId,
6
+ readBooleanField,
7
+ readNumberField,
8
+ readStringField,
9
+ resetAssessmentWarningsForTests,
10
+ useAssessmentHandleRegistration,
11
+ usePluginScoring,
12
+ useQuizState
13
+ } from "./chunk-TDM3ARE7.js";
14
+
15
+ // src/runtime/lessonMountRegistry.ts
16
+ var mountCounts = /* @__PURE__ */ new Map();
17
+ var warnedConcurrentLessons = false;
18
+ function registerLessonMount(lessonId) {
19
+ if (isDevEnvironment() && mountCounts.size > 0 && !mountCounts.has(lessonId) && !warnedConcurrentLessons) {
20
+ warnedConcurrentLessons = true;
21
+ console.warn(
22
+ "[lessonkit] Multiple <Lesson> components are mounted; only one should be active at a time. Set autoCompleteOnUnmount={false} on routed lessons or unmount the previous lesson before showing the next."
23
+ );
24
+ }
25
+ mountCounts.set(lessonId, (mountCounts.get(lessonId) ?? 0) + 1);
26
+ return () => {
27
+ const next = (mountCounts.get(lessonId) ?? 1) - 1;
28
+ if (next <= 0) {
29
+ mountCounts.delete(lessonId);
30
+ } else {
31
+ mountCounts.set(lessonId, next);
32
+ }
33
+ };
34
+ }
35
+ function getLessonMountCount(lessonId) {
36
+ return mountCounts.get(lessonId) ?? 0;
37
+ }
38
+ function resetLessonMountRegistryForTests() {
39
+ mountCounts.clear();
40
+ warnedConcurrentLessons = false;
41
+ }
42
+
43
+ // src/components/Quiz.tsx
44
+ import { forwardRef, useEffect, useId, useMemo, useRef, useState } from "react";
45
+ import { visuallyHiddenStyle } from "@lessonkit/accessibility";
46
+ import { jsx, jsxs } from "react/jsx-runtime";
47
+ function QuizInner(props, ref) {
48
+ const { enclosingLessonId } = props;
49
+ const checkId = useMemo(() => normalizeComponentId(props.checkId, "checkId"), [props.checkId]);
50
+ const quiz = useQuizState(enclosingLessonId);
51
+ const { getPluginScore, isChoiceCorrect } = usePluginScoring(checkId, enclosingLessonId);
52
+ const [selected, setSelected] = useState(null);
53
+ const [selectionCorrect, setSelectionCorrect] = useState(null);
54
+ const [quizPassed, setQuizPassed] = useState(false);
55
+ const [completedScore, setCompletedScore] = useState(null);
56
+ const [completedMaxScore, setCompletedMaxScore] = useState(null);
57
+ const completedRef = useRef(false);
58
+ const telemetryReplayedRef = useRef(false);
59
+ const questionId = useId();
60
+ const choicesKey = props.choices.join("\0");
61
+ useEffect(() => {
62
+ completedRef.current = false;
63
+ telemetryReplayedRef.current = false;
64
+ setQuizPassed(false);
65
+ setSelected(null);
66
+ setSelectionCorrect(null);
67
+ setCompletedScore(null);
68
+ setCompletedMaxScore(null);
69
+ }, [checkId, props.answer, props.question, choicesKey]);
70
+ const passed = quizPassed;
71
+ const resolveScores = () => {
72
+ const maxScore = completedMaxScore ?? 1;
73
+ if (quizPassed) {
74
+ return { score: completedScore ?? maxScore, maxScore };
75
+ }
76
+ if (selected !== null && selectionCorrect) {
77
+ return { score: completedMaxScore ?? maxScore, maxScore };
78
+ }
79
+ return { score: 0, maxScore };
80
+ };
81
+ const replayTelemetry = (nextSelected, nextCorrect, nextPassed, nextScore, nextMaxScore) => {
82
+ if (!nextPassed || telemetryReplayedRef.current) return;
83
+ telemetryReplayedRef.current = true;
84
+ if (nextSelected !== null) {
85
+ quiz.answer({
86
+ checkId,
87
+ question: props.question,
88
+ choice: nextSelected,
89
+ correct: nextCorrect ?? false
90
+ });
91
+ }
92
+ quiz.complete({
93
+ checkId,
94
+ score: nextScore,
95
+ maxScore: nextMaxScore,
96
+ passingScore: props.passingScore ?? nextMaxScore
97
+ });
98
+ };
99
+ const handle = useMemo(
100
+ () => buildAssessmentHandle({
101
+ checkId,
102
+ getScore: () => resolveScores().score,
103
+ getMaxScore: () => resolveScores().maxScore,
104
+ getAnswerGiven: () => selected !== null,
105
+ resetTask: () => {
106
+ completedRef.current = false;
107
+ telemetryReplayedRef.current = false;
108
+ setQuizPassed(false);
109
+ setSelected(null);
110
+ setSelectionCorrect(null);
111
+ setCompletedScore(null);
112
+ setCompletedMaxScore(null);
113
+ },
114
+ showSolutions: () => {
115
+ },
116
+ getXAPIData: () => {
117
+ const { score, maxScore } = resolveScores();
118
+ return {
119
+ checkId,
120
+ interactionType: "mcq",
121
+ response: selected ?? void 0,
122
+ correct: selectionCorrect ?? void 0,
123
+ score,
124
+ maxScore
125
+ };
126
+ },
127
+ getCurrentState: () => ({
128
+ selected,
129
+ selectionCorrect,
130
+ quizPassed,
131
+ completedScore,
132
+ completedMaxScore
133
+ }),
134
+ resume: (state) => {
135
+ const nextSelected = readStringField(state, "selected");
136
+ if (typeof nextSelected === "string" || nextSelected === null) setSelected(nextSelected);
137
+ const nextCorrect = readBooleanField(state, "selectionCorrect");
138
+ if (nextCorrect === true || nextCorrect === false || nextCorrect === null) {
139
+ setSelectionCorrect(nextCorrect);
140
+ }
141
+ const nextCompletedScore = readNumberField(state, "completedScore");
142
+ if (typeof nextCompletedScore === "number") setCompletedScore(nextCompletedScore);
143
+ const nextCompletedMaxScore = readNumberField(state, "completedMaxScore");
144
+ if (typeof nextCompletedMaxScore === "number") setCompletedMaxScore(nextCompletedMaxScore);
145
+ const nextPassed = readBooleanField(state, "quizPassed");
146
+ if (nextPassed === true || nextPassed === false) {
147
+ setQuizPassed(nextPassed);
148
+ completedRef.current = nextPassed;
149
+ if (nextPassed) {
150
+ const maxScore = nextCompletedMaxScore ?? completedMaxScore ?? 1;
151
+ const score = nextCompletedScore ?? completedScore ?? maxScore;
152
+ replayTelemetry(
153
+ nextSelected ?? null,
154
+ nextCorrect ?? null,
155
+ nextPassed,
156
+ score,
157
+ maxScore
158
+ );
159
+ }
160
+ }
161
+ }
162
+ }),
163
+ [
164
+ checkId,
165
+ completedMaxScore,
166
+ completedScore,
167
+ props.passingScore,
168
+ props.question,
169
+ quiz,
170
+ quizPassed,
171
+ selected,
172
+ selectionCorrect
173
+ ]
174
+ );
175
+ useAssessmentHandleRegistration(checkId, handle, ref);
176
+ return /* @__PURE__ */ jsxs("section", { "aria-label": "Quiz", "data-lk-check-id": checkId, children: [
177
+ /* @__PURE__ */ jsx("p", { id: questionId, children: props.question }),
178
+ /* @__PURE__ */ jsxs("fieldset", { "aria-labelledby": questionId, children: [
179
+ /* @__PURE__ */ jsx("legend", { style: visuallyHiddenStyle, children: "Quiz choices" }),
180
+ props.choices.map((c, i) => /* @__PURE__ */ jsxs("label", { style: { display: "block" }, children: [
181
+ /* @__PURE__ */ jsx(
182
+ "input",
183
+ {
184
+ type: "radio",
185
+ name: questionId,
186
+ value: c,
187
+ checked: selected === c,
188
+ disabled: passed,
189
+ "aria-invalid": selected === c && selectionCorrect === false ? true : void 0,
190
+ onChange: () => {
191
+ if (passed) return;
192
+ setSelected(c);
193
+ const custom = getPluginScore(c);
194
+ const correct = isChoiceCorrect(c, props.answer, custom, props.passingScore);
195
+ setSelectionCorrect(correct);
196
+ quiz.answer({
197
+ checkId,
198
+ question: props.question,
199
+ choice: c,
200
+ correct
201
+ });
202
+ if (correct && !completedRef.current) {
203
+ completedRef.current = true;
204
+ setQuizPassed(true);
205
+ const maxScore = custom?.maxScore ?? 1;
206
+ const score = custom?.score ?? maxScore;
207
+ setCompletedScore(score);
208
+ setCompletedMaxScore(maxScore);
209
+ quiz.complete({
210
+ checkId,
211
+ score,
212
+ maxScore,
213
+ passingScore: props.passingScore ?? maxScore
214
+ });
215
+ }
216
+ }
217
+ }
218
+ ),
219
+ c
220
+ ] }, `${questionId}-${i}`))
221
+ ] }),
222
+ selected && selectionCorrect !== null ? /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", children: selectionCorrect ? "Correct" : "Try again" }) : null
223
+ ] });
224
+ }
225
+ var QuizInnerForwarded = forwardRef(QuizInner);
226
+ var Quiz = forwardRef(function Quiz2(props, ref) {
227
+ return /* @__PURE__ */ jsx(AssessmentLessonGuard, { blockLabel: "Quiz", checkId: props.checkId, children: (lessonId) => /* @__PURE__ */ jsx(QuizInnerForwarded, { ...props, enclosingLessonId: lessonId, ref }) });
228
+ });
229
+ function KnowledgeCheck(props) {
230
+ return /* @__PURE__ */ jsx(
231
+ Quiz,
232
+ {
233
+ checkId: props.checkId,
234
+ question: props.question,
235
+ choices: props.choices,
236
+ answer: props.answer,
237
+ passingScore: props.passingScore
238
+ }
239
+ );
240
+ }
241
+ function resetQuizWarningsForTests() {
242
+ resetAssessmentWarningsForTests();
243
+ }
244
+
245
+ export {
246
+ registerLessonMount,
247
+ getLessonMountCount,
248
+ resetLessonMountRegistryForTests,
249
+ Quiz,
250
+ KnowledgeCheck,
251
+ resetQuizWarningsForTests
252
+ };