@intellectif/lk-react 1.0.1 → 2.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.
Files changed (66) hide show
  1. package/dist/{chunk-5KW7TFX3.js → chunk-3X7MYAFT.js} +110 -29
  2. package/dist/chunk-3X7MYAFT.js.map +1 -0
  3. package/dist/chunk-7BT6OXAR.cjs +225 -0
  4. package/dist/chunk-7BT6OXAR.cjs.map +1 -0
  5. package/dist/{chunk-IZNMQSQP.js → chunk-E4MNA2IQ.js} +17 -3
  6. package/dist/chunk-E4MNA2IQ.js.map +1 -0
  7. package/dist/{chunk-TVK3SVV6.cjs → chunk-IYLQSNLE.cjs} +17 -3
  8. package/dist/chunk-IYLQSNLE.cjs.map +1 -0
  9. package/dist/{chunk-6OBRYL4C.cjs → chunk-QHOV2A6M.cjs} +11 -2
  10. package/dist/chunk-QHOV2A6M.cjs.map +1 -0
  11. package/dist/{chunk-RI53A36N.cjs → chunk-STLRKR2I.cjs} +18 -5
  12. package/dist/chunk-STLRKR2I.cjs.map +1 -0
  13. package/dist/{chunk-ZPD7BGCF.js → chunk-UASXUWHH.js} +18 -5
  14. package/dist/chunk-UASXUWHH.js.map +1 -0
  15. package/dist/{chunk-L3EV5LIX.js → chunk-UIGIYYGY.js} +11 -3
  16. package/dist/chunk-UIGIYYGY.js.map +1 -0
  17. package/dist/{chunk-O3GWWJFS.cjs → chunk-UXO6KYSZ.cjs} +112 -31
  18. package/dist/chunk-UXO6KYSZ.cjs.map +1 -0
  19. package/dist/chunk-W6BH2YMZ.cjs +259 -0
  20. package/dist/chunk-W6BH2YMZ.cjs.map +1 -0
  21. package/dist/chunk-XTJ22JAP.js +223 -0
  22. package/dist/chunk-XTJ22JAP.js.map +1 -0
  23. package/dist/chunk-ZVFWJTT4.js +257 -0
  24. package/dist/chunk-ZVFWJTT4.js.map +1 -0
  25. package/dist/components/ActivitySequence.cjs +5 -5
  26. package/dist/components/ActivitySequence.js +4 -4
  27. package/dist/components/FillInTheBlanks.cjs +3 -3
  28. package/dist/components/FillInTheBlanks.d.cts +7 -1
  29. package/dist/components/FillInTheBlanks.d.ts +7 -1
  30. package/dist/components/FillInTheBlanks.js +2 -2
  31. package/dist/components/MultipleChoice.cjs +3 -3
  32. package/dist/components/MultipleChoice.d.cts +14 -3
  33. package/dist/components/MultipleChoice.d.ts +14 -3
  34. package/dist/components/MultipleChoice.js +2 -2
  35. package/dist/components/WrittenResponse.cjs +14 -0
  36. package/dist/components/WrittenResponse.cjs.map +1 -0
  37. package/dist/components/WrittenResponse.d.cts +96 -0
  38. package/dist/components/WrittenResponse.d.ts +96 -0
  39. package/dist/components/WrittenResponse.js +5 -0
  40. package/dist/components/WrittenResponse.js.map +1 -0
  41. package/dist/hooks/useXAPI.cjs +2 -2
  42. package/dist/hooks/useXAPI.js +1 -1
  43. package/dist/index.cjs +19 -10
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.d.cts +3 -2
  46. package/dist/index.d.ts +3 -2
  47. package/dist/index.js +13 -5
  48. package/dist/index.js.map +1 -1
  49. package/dist/theme/skin.css +85 -0
  50. package/dist/types-CNmjqD6M.d.cts +108 -0
  51. package/dist/types-CNmjqD6M.d.ts +108 -0
  52. package/package.json +13 -4
  53. package/dist/chunk-5KW7TFX3.js.map +0 -1
  54. package/dist/chunk-6OBRYL4C.cjs.map +0 -1
  55. package/dist/chunk-IZNMQSQP.js.map +0 -1
  56. package/dist/chunk-L3EV5LIX.js.map +0 -1
  57. package/dist/chunk-O3GWWJFS.cjs.map +0 -1
  58. package/dist/chunk-QBA6UXMS.cjs +0 -160
  59. package/dist/chunk-QBA6UXMS.cjs.map +0 -1
  60. package/dist/chunk-RI53A36N.cjs.map +0 -1
  61. package/dist/chunk-TVK3SVV6.cjs.map +0 -1
  62. package/dist/chunk-W2L66WHW.js +0 -158
  63. package/dist/chunk-W2L66WHW.js.map +0 -1
  64. package/dist/chunk-ZPD7BGCF.js.map +0 -1
  65. package/dist/types-iknDq5tf.d.cts +0 -17
  66. package/dist/types-iknDq5tf.d.ts +0 -17
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunk6OBRYL4C_cjs = require('./chunk-6OBRYL4C.cjs');
3
+ var chunkQHOV2A6M_cjs = require('./chunk-QHOV2A6M.cjs');
4
4
  var chunkHD2AIQRD_cjs = require('./chunk-HD2AIQRD.cjs');
5
5
  var lkCore = require('@intellectif/lk-core');
6
6
  var react = require('react');
@@ -27,55 +27,106 @@ function parsePassage(passage) {
27
27
  }
28
28
  return segments;
29
29
  }
30
+ var NO_ANSWERS = {};
31
+ function answersOf(response) {
32
+ return response !== void 0 && response.type === "fill-in-the-blanks" ? response.answers : NO_ANSWERS;
33
+ }
34
+ function correctnessByItem(details) {
35
+ const map = /* @__PURE__ */ new Map();
36
+ for (const detail of details) {
37
+ map.set(
38
+ detail.itemId,
39
+ detail.outcome !== void 0 ? detail.outcome === "correct" : detail.correct
40
+ );
41
+ }
42
+ return map;
43
+ }
30
44
  function FillInTheBlanks({
31
45
  data,
32
46
  onComplete,
47
+ onSubmit,
48
+ onChange,
33
49
  onInteraction,
50
+ value,
51
+ defaultValue,
52
+ renderMode = "practice",
53
+ outcome,
54
+ sanitizeHtml,
34
55
  theme,
35
56
  locale,
36
57
  disabled,
37
58
  showCorrectAnswers
38
59
  }) {
60
+ const isExam = renderMode === "exam";
61
+ const isReview = renderMode === "review";
39
62
  const devError = react.useMemo(() => {
40
- if (!chunk6OBRYL4C_cjs.isDevelopment()) {
63
+ if (!chunkQHOV2A6M_cjs.isDevelopment()) {
41
64
  return null;
42
65
  }
66
+ if (data.redacted === true) {
67
+ try {
68
+ lkCore.assertRedacted(data);
69
+ return null;
70
+ } catch (error) {
71
+ return error instanceof Error ? error : new Error(String(error));
72
+ }
73
+ }
43
74
  const result2 = lkCore.validateActivity("fill-in-the-blanks", data);
44
75
  return result2.success ? null : new lkCore.ActivitySchemaError("fill-in-the-blanks", result2.errors);
45
76
  }, [data]);
46
77
  const { state, start, complete, getTimeSpent, reset } = chunkHD2AIQRD_cjs.useActivityState();
47
- const [answers, setAnswers] = react.useState({});
78
+ const isControlled = value !== void 0;
79
+ const [internalAnswers, setInternalAnswers] = react.useState(
80
+ () => answersOf(defaultValue)
81
+ );
48
82
  const [revealed, setRevealed] = react.useState(() => /* @__PURE__ */ new Set());
49
83
  const [result, setResult] = react.useState(null);
50
84
  const [summary, setSummary] = react.useState(null);
51
85
  const [feedbackHidden, setFeedbackHidden] = react.useState(false);
86
+ const answers = isControlled ? answersOf(value) : internalAnswers;
87
+ const defaultValueRef = react.useRef(defaultValue);
52
88
  react.useEffect(() => {
53
- setAnswers({});
89
+ defaultValueRef.current = defaultValue;
90
+ }, [defaultValue]);
91
+ react.useEffect(() => {
92
+ setInternalAnswers(answersOf(defaultValueRef.current));
54
93
  setRevealed(/* @__PURE__ */ new Set());
55
94
  setResult(null);
56
95
  setSummary(null);
57
96
  setFeedbackHidden(false);
58
97
  reset();
59
98
  }, [data, reset]);
99
+ react.useEffect(() => {
100
+ if (chunkQHOV2A6M_cjs.isDevelopment() && sanitizeHtml !== void 0 && data.passageHtml !== void 0) {
101
+ console.warn(
102
+ "[lk-react] FillInTheBlanks ignores `passageHtml`: the passage hosts the answer inputs, so rich HTML cannot be split at {{placeholders}} without voiding the sanitiser. Rendering the plain `passage` instead."
103
+ );
104
+ }
105
+ }, [sanitizeHtml, data.passageHtml]);
60
106
  const segments = react.useMemo(() => parsePassage(data.passage), [data.passage]);
61
107
  const blankById = react.useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);
62
108
  if (devError) {
63
109
  throw devError;
64
110
  }
65
111
  const submitted = state === "completed";
66
- const inactive = disabled === true || submitted;
112
+ const inactive = disabled === true || submitted || isReview;
113
+ const revealing = isReview ? outcome?.status === "scored" : !isExam && submitted;
67
114
  const fireInteraction = (type, payload) => {
68
115
  onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
69
116
  };
70
- const handleChange = (blankId, value) => {
117
+ const handleChange = (blankId, text) => {
71
118
  if (inactive) {
72
119
  return;
73
120
  }
74
121
  if (state === "idle") {
75
122
  start();
76
123
  }
77
- setAnswers((prev) => ({ ...prev, [blankId]: value }));
78
- fireInteraction("blank-filled", { blankId, value });
124
+ const next = { ...answers, [blankId]: text };
125
+ if (!isControlled) {
126
+ setInternalAnswers(next);
127
+ }
128
+ onChange?.({ type: "fill-in-the-blanks", answers: next });
129
+ fireInteraction("blank-filled", { blankId, value: text });
79
130
  };
80
131
  const toggleHint = (blankId) => {
81
132
  if (inactive) {
@@ -103,47 +154,74 @@ function FillInTheBlanks({
103
154
  return;
104
155
  }
105
156
  const response = { type: "fill-in-the-blanks", answers };
157
+ onSubmit?.(response);
158
+ if (isExam) {
159
+ complete();
160
+ setSummary("Answer submitted.");
161
+ fireInteraction("submitted", { answers });
162
+ return;
163
+ }
106
164
  const scoringResult = lkCore.score("fill-in-the-blanks", data, response);
107
165
  complete();
108
166
  const timeSpent = getTimeSpent();
109
167
  const xapiStatement = lkCore.xAPIBuilder.buildAnsweredStatement({
110
- actor: chunk6OBRYL4C_cjs.ANONYMOUS_ACTOR,
111
- object: { id: chunk6OBRYL4C_cjs.objectIdFor(data.id), name: { "en-US": data.title } },
168
+ actor: chunkQHOV2A6M_cjs.ANONYMOUS_ACTOR,
169
+ object: {
170
+ id: chunkQHOV2A6M_cjs.objectIdFor(data.id),
171
+ name: { [data.locale ?? "en-US"]: data.title },
172
+ type: "http://adlnet.gov/expapi/activities/cmi.interaction",
173
+ interactionType: "fill-in",
174
+ correctResponsesPattern: [
175
+ data.blanks.map((blank) => blank.acceptedAnswers[0] ?? "").join("[,]")
176
+ ]
177
+ },
112
178
  scoringResult,
113
179
  timeSpentMs: timeSpent,
114
180
  response: JSON.stringify(answers)
115
181
  });
116
182
  setResult(scoringResult);
117
- onComplete({
183
+ onComplete?.({
118
184
  score: scoringResult.score,
119
185
  maxScore: scoringResult.maxScore,
120
186
  passed: scoringResult.passed,
121
187
  timeSpent,
122
188
  xapiStatement
123
189
  });
124
- const overall = scoringResult.passed ? data.feedback?.correct : data.feedback?.incorrect;
190
+ const overall = scoringResult.feedback;
125
191
  setSummary(
126
192
  `Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${scoringResult.passed ? "Passed." : "Not passed."}${overall ? ` ${overall}` : ""}`
127
193
  );
128
194
  fireInteraction("submitted", { answers, score: scoringResult.score });
129
195
  };
130
196
  const correctByBlank = react.useMemo(() => {
131
- const map = /* @__PURE__ */ new Map();
132
- for (const detail of result?.details ?? []) {
133
- map.set(detail.itemId, detail.correct);
197
+ if (isExam) {
198
+ return /* @__PURE__ */ new Map();
199
+ }
200
+ if (isReview) {
201
+ return outcome?.status === "scored" ? correctnessByItem(outcome.details) : /* @__PURE__ */ new Map();
202
+ }
203
+ return correctnessByItem(result?.details ?? []);
204
+ }, [isExam, isReview, outcome, result]);
205
+ const reviewSummary = react.useMemo(() => {
206
+ if (!isReview || outcome === void 0) {
207
+ return null;
208
+ }
209
+ if (outcome.status === "scored") {
210
+ return `Score ${Math.round(outcome.score * 100)}%. ${outcome.passed ? "Passed." : "Not passed."}${outcome.feedback ? ` ${outcome.feedback}` : ""}`;
134
211
  }
135
- return map;
136
- }, [result]);
212
+ return outcome.status === "deferred" ? "Not graded yet." : "No grade available.";
213
+ }, [isReview, outcome]);
137
214
  return /* @__PURE__ */ jsxRuntime.jsxs(
138
215
  "form",
139
216
  {
140
217
  className: "lk-fib",
141
218
  "aria-label": data.title,
142
219
  lang: locale,
220
+ "data-render-mode": renderMode,
143
221
  style: theme,
144
222
  onSubmit: handleSubmit,
145
223
  children: [
146
- data.media ? /* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.ActivityMedia, { media: data.media }) : null,
224
+ data.media ? /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.ActivityMedia, { media: data.media }) : null,
147
225
  /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { disabled: inactive, children: [
148
226
  /* @__PURE__ */ jsxRuntime.jsx("p", { className: "lk-fib-passage", children: segments.map((seg, i) => {
149
227
  if (seg.kind === "text") {
@@ -153,10 +231,13 @@ function FillInTheBlanks({
153
231
  if (!blank) {
154
232
  return null;
155
233
  }
156
- const isCorrect = correctByBlank.get(seg.id) === true;
234
+ const acceptedAnswers = isExam ? void 0 : blank.acceptedAnswers;
235
+ const blankFeedback = isExam ? void 0 : blank.feedback;
236
+ const correctness = correctByBlank.get(seg.id);
157
237
  const hintId = `${data.id}-hint-${seg.id}`;
158
- if (submitted && showCorrectAnswers && !isCorrect) {
159
- return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: blank.acceptedAnswers[0] }, seg.id);
238
+ const correctAnswer = acceptedAnswers?.[0];
239
+ if (revealing && showCorrectAnswers && correctness === false && correctAnswer) {
240
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-fib-answer", "data-correct": "false", children: correctAnswer }, seg.id);
160
241
  }
161
242
  return /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "lk-fib-blank", children: [
162
243
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -168,7 +249,7 @@ function FillInTheBlanks({
168
249
  value: answers[seg.id] ?? "",
169
250
  disabled: inactive,
170
251
  "aria-disabled": inactive || void 0,
171
- "data-correct": submitted ? String(isCorrect) : void 0,
252
+ "data-correct": revealing && correctness !== void 0 ? String(correctness) : void 0,
172
253
  onChange: (e) => handleChange(seg.id, e.target.value)
173
254
  }
174
255
  ),
@@ -205,20 +286,20 @@ function FillInTheBlanks({
205
286
  ),
206
287
  /* @__PURE__ */ jsxRuntime.jsx("span", { id: hintId, "aria-live": "polite", children: revealed.has(seg.id) ? blank.hint : "" })
207
288
  ] }) : null,
208
- submitted && !feedbackHidden && blank.feedback ? /* @__PURE__ */ jsxRuntime.jsx(
289
+ revealing && !feedbackHidden && blankFeedback ? /* @__PURE__ */ jsxRuntime.jsx(
209
290
  "span",
210
291
  {
211
292
  className: "lk-fib-blank-feedback",
212
293
  role: "note",
213
- "data-correct": String(isCorrect),
214
- children: blank.feedback
294
+ "data-correct": correctness !== void 0 ? String(correctness) : void 0,
295
+ children: blankFeedback
215
296
  }
216
297
  ) : null
217
298
  ] }, seg.id);
218
299
  }) }),
219
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: "Check answers" })
300
+ isReview ? null : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: isExam ? "Submit answers" : "Check answers" })
220
301
  ] }),
221
- submitted && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsxRuntime.jsx(
302
+ revealing && data.blanks.some((b) => b.feedback) ? /* @__PURE__ */ jsxRuntime.jsx(
222
303
  "button",
223
304
  {
224
305
  type: "button",
@@ -228,15 +309,15 @@ function FillInTheBlanks({
228
309
  children: feedbackHidden ? "Show feedback" : "Hide feedback"
229
310
  }
230
311
  ) : null,
231
- /* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: summary })
312
+ /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: isReview ? reviewSummary : summary })
232
313
  ]
233
314
  }
234
315
  );
235
316
  }
236
317
  function FillInTheBlanks2(props) {
237
- return /* @__PURE__ */ jsxRuntime.jsx(chunk6OBRYL4C_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsxRuntime.jsx(FillInTheBlanks, { ...props }) });
318
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsxRuntime.jsx(FillInTheBlanks, { ...props }) });
238
319
  }
239
320
 
240
321
  exports.FillInTheBlanks = FillInTheBlanks2;
241
- //# sourceMappingURL=chunk-O3GWWJFS.cjs.map
242
- //# sourceMappingURL=chunk-O3GWWJFS.cjs.map
322
+ //# sourceMappingURL=chunk-UXO6KYSZ.cjs.map
323
+ //# sourceMappingURL=chunk-UXO6KYSZ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/components/FillInTheBlanks/FillInTheBlanks.tsx","../src/components/FillInTheBlanks/index.tsx"],"names":["useMemo","isDevelopment","assertRedacted","result","validateActivity","ActivitySchemaError","useActivityState","useState","useRef","useEffect","score","xAPIBuilder","ANONYMOUS_ACTOR","objectIdFor","jsxs","jsx","ActivityMedia","Fragment","FeedbackRegion","FillInTheBlanks","ActivityErrorBoundary"],"mappings":"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","file":"chunk-UXO6KYSZ.cjs","sourcesContent":["'use client';\n\nimport {\n ActivitySchemaError,\n assertRedacted,\n type FillInTheBlanksData,\n type FillInTheBlanksLearnerResponse,\n type LearnerResponse,\n type ScoringDetail,\n type ScoringResult,\n score,\n validateActivity,\n xAPIBuilder,\n} from '@intellectif/lk-core';\nimport { type CSSProperties, useEffect, useMemo, useRef, useState } from 'react';\nimport { useActivityState } from '../../hooks/useActivityState.js';\nimport { ANONYMOUS_ACTOR, isDevelopment, objectIdFor } from '../_internal.js';\nimport { ActivityMedia } from '../shared/ActivityMedia.js';\nimport { FeedbackRegion } from '../shared/FeedbackRegion.js';\nimport type { ActivityProps } from '../types.js';\n\nexport interface FillInTheBlanksProps extends ActivityProps<FillInTheBlanksData> {\n /**\n * After submission, replace each incorrectly-answered blank with its first\n * accepted answer, styled distinctly (Req 5.7). Component-specific prop: it\n * is presentation, not activity data, and is outside the fixed Req-3.1 set.\n *\n * Ignored in `exam` mode — revealing the key is precisely what an exam must\n * not do, and on a `redact()` projection there is no key to reveal. In\n * `review` mode it applies only to blanks the supplied `outcome` marks\n * incorrect, and only when the caller passed data that still carries the key\n * (e.g. `redact(data, { reveal: 'after-submit' })`).\n */\n showCorrectAnswers?: boolean;\n}\n\nconst PLACEHOLDER = /\\{\\{\\s*([^{}]+?)\\s*\\}\\}/g;\n\ntype Segment = { kind: 'text'; value: string } | { kind: 'blank'; id: string; ordinal: number };\n\nfunction parsePassage(passage: string): Segment[] {\n const segments: Segment[] = [];\n const re = new RegExp(PLACEHOLDER);\n let last = 0;\n let ordinal = 0;\n let match = re.exec(passage);\n while (match !== null) {\n if (match.index > last) {\n segments.push({ kind: 'text', value: passage.slice(last, match.index) });\n }\n ordinal += 1;\n segments.push({ kind: 'blank', id: match[1] as string, ordinal });\n last = match.index + match[0].length;\n match = re.exec(passage);\n }\n if (last < passage.length) {\n segments.push({ kind: 'text', value: passage.slice(last) });\n }\n return segments;\n}\n\n/** Shared empty map: keeps the \"no response supplied\" path allocation-free. */\nconst NO_ANSWERS: Record<string, string> = {};\n\n/**\n * Reads the answer map out of a `LearnerResponse`. A response of another\n * activity type is treated as \"no answers\" rather than throwing: a controlled\n * host that has not yet swapped its state on an activity change would\n * otherwise crash the attempt.\n */\nfunction answersOf(response: LearnerResponse | undefined): Record<string, string> {\n return response !== undefined && response.type === 'fill-in-the-blanks'\n ? response.answers\n : NO_ANSWERS;\n}\n\n/**\n * Per-item correctness keyed by blank id. Prefers the unambiguous `outcome`\n * field (always written by the built-in scorers since lk-core 0.3.0) and\n * falls back to the deprecated `correct` boolean so a 0.2-era\n * consumer-constructed `ItemOutcome` still marks correctly in `review`.\n */\nfunction correctnessByItem(details: readonly ScoringDetail[]): Map<string, boolean> {\n const map = new Map<string, boolean>();\n for (const detail of details) {\n map.set(\n detail.itemId,\n detail.outcome !== undefined ? detail.outcome === 'correct' : detail.correct,\n );\n }\n return map;\n}\n\nexport function FillInTheBlanks({\n data,\n onComplete,\n onSubmit,\n onChange,\n onInteraction,\n value,\n defaultValue,\n renderMode = 'practice',\n outcome,\n sanitizeHtml,\n theme,\n locale,\n disabled,\n showCorrectAnswers,\n}: FillInTheBlanksProps) {\n const isExam = renderMode === 'exam';\n const isReview = renderMode === 'review';\n\n // Dev-only boundary validation (Req 2.3); throws in render so the wrapping\n // ActivityErrorBoundary catches it. Re-runs only when data changes.\n const devError = useMemo(() => {\n if (!isDevelopment()) {\n return null;\n }\n if (data.redacted === true) {\n // A redact() projection cannot satisfy the full content schema — the\n // answer key is gone by design. Validate it against the STRICT redacted\n // schema instead, which additionally proves the payload is learner-safe.\n try {\n assertRedacted(data);\n return null;\n } catch (error) {\n return error instanceof Error ? error : new Error(String(error));\n }\n }\n const result = validateActivity('fill-in-the-blanks', data);\n return result.success ? null : new ActivitySchemaError('fill-in-the-blanks', result.errors);\n }, [data]);\n\n const { state, start, complete, getTimeSpent, reset } = useActivityState();\n // Controlled when `value` is supplied: the answers rendered are ALWAYS the\n // caller's, and internal state is never read. Uncontrolled otherwise,\n // seeded from `defaultValue`.\n const isControlled = value !== undefined;\n const [internalAnswers, setInternalAnswers] = useState<Record<string, string>>(() =>\n answersOf(defaultValue),\n );\n const [revealed, setRevealed] = useState<Set<string>>(() => new Set());\n const [result, setResult] = useState<ScoringResult | null>(null);\n const [summary, setSummary] = useState<string | null>(null);\n const [feedbackHidden, setFeedbackHidden] = useState(false);\n\n const answers = isControlled ? answersOf(value) : internalAnswers;\n\n // `defaultValue` is read only by the reset effect, and it is commonly an\n // inline object literal — putting it in that effect's deps would re-seed\n // (and wipe) the learner's typing on every render. Mirror it into a ref in\n // its own effect, which commits before the reset effect below.\n const defaultValueRef = useRef(defaultValue);\n useEffect(() => {\n defaultValueRef.current = defaultValue;\n }, [defaultValue]);\n\n // `data` is an intentional reset trigger (Req 3.7), not read in the body.\n // biome-ignore lint/correctness/useExhaustiveDependencies: data is the reset trigger (Req 3.7)\n useEffect(() => {\n setInternalAnswers(answersOf(defaultValueRef.current));\n setRevealed(new Set());\n setResult(null);\n setSummary(null);\n setFeedbackHidden(false);\n reset();\n }, [data, reset]);\n\n /*\n * RICH TEXT — deliberate, documented limitation (see `sanitizeHtml`).\n *\n * Every other activity can render its `*Html` field through the caller's\n * sanitiser, because the rich text is a LEAF: the sanitised string is\n * handed to one `dangerouslySetInnerHTML` node and nothing is inserted into\n * it. A fill-in-the-blanks passage is not a leaf — it is the CONTAINER of\n * the React-controlled `<input>`s, one per `{{id}}` placeholder.\n *\n * Placing those inputs inside sanitised HTML means slicing the sanitised\n * string at the placeholders and injecting each slice as its own HTML\n * fragment, which is unsafe and broken on both counts:\n * 1. Broken: the slices are unbalanced. `<p>A {{x}} B</p>` yields `<p>A `\n * and ` B</p>`; the fragment parser auto-closes both, so the input\n * lands OUTSIDE the paragraph and the authored block structure is\n * destroyed.\n * 2. Unsafe: the sanitiser's guarantee covers the document it returned,\n * not arbitrary substrings of it. A placeholder inside an attribute\n * (`<img alt=\"{{x}}\">`) splits mid-attribute, and re-parsing the\n * halves in a fresh context is exactly the mutation (mXSS) class of\n * transformation that voids a sanitiser's output guarantee. An exam\n * runner must never depend on that.\n *\n * The safe alternative — parse the sanitised string once into a detached\n * DOM, split only TEXT nodes at placeholders, and portal the inputs into\n * anchors — needs a DOM at render time (no SSR) and a large imperative\n * subsystem. Until that lands, the plain-text `passage` is rendered (always\n * escaped by React), which is correct, answerable, and safe in every mode.\n * `data.passageHtml` is carried and redacted by lk-core regardless, so no\n * authored content is lost — only this component declines to render it.\n */\n useEffect(() => {\n if (isDevelopment() && sanitizeHtml !== undefined && data.passageHtml !== undefined) {\n console.warn(\n '[lk-react] FillInTheBlanks ignores `passageHtml`: the passage hosts the answer inputs, ' +\n 'so rich HTML cannot be split at {{placeholders}} without voiding the sanitiser. ' +\n 'Rendering the plain `passage` instead.',\n );\n }\n }, [sanitizeHtml, data.passageHtml]);\n\n const segments = useMemo(() => parsePassage(data.passage), [data.passage]);\n const blankById = useMemo(() => new Map(data.blanks.map((b) => [b.id, b])), [data.blanks]);\n\n if (devError) {\n throw devError;\n }\n\n const submitted = state === 'completed';\n const inactive = disabled === true || submitted || isReview;\n\n /**\n * Whether the component may show answer-key-derived information (per-blank\n * correctness, authored feedback, correct answers). NEVER in `exam`; in\n * `practice` only after the component scored locally; in `review` only once\n * the caller supplies a SCORED outcome — an item that is still awaiting a\n * grade must not have its key revealed just because it is being read back.\n */\n const revealing = isReview ? outcome?.status === 'scored' : !isExam && submitted;\n\n const fireInteraction = (\n type: 'blank-filled' | 'hint-requested' | 'submitted',\n payload: Record<string, unknown>,\n ): void => {\n onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });\n };\n\n const handleChange = (blankId: string, text: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n const next = { ...answers, [blankId]: text };\n if (!isControlled) {\n setInternalAnswers(next);\n }\n onChange?.({ type: 'fill-in-the-blanks', answers: next });\n fireInteraction('blank-filled', { blankId, value: text });\n };\n\n const toggleHint = (blankId: string): void => {\n if (inactive) {\n return;\n }\n if (state === 'idle') {\n start();\n }\n setRevealed((prev) => {\n const next = new Set(prev);\n if (next.has(blankId)) {\n next.delete(blankId);\n return next;\n }\n next.add(blankId);\n return next;\n });\n // Only a reveal is a \"hint requested\"; hiding is not a new request.\n if (!revealed.has(blankId)) {\n fireInteraction('hint-requested', { blankId });\n }\n };\n\n const handleSubmit = (event: React.FormEvent): void => {\n event.preventDefault();\n if (inactive) {\n return;\n }\n const response: FillInTheBlanksLearnerResponse = { type: 'fill-in-the-blanks', answers };\n // Fires in every submitting mode and always BEFORE onComplete, so an exam\n // runner can persist the raw response regardless of local grading.\n onSubmit?.(response);\n\n if (isExam) {\n // EXAM: the client neither grades nor reveals. No score()/evaluate(), no\n // read of any answer-key field, no onComplete — and no xAPI statement,\n // because its `correctResponsesPattern` IS the answer key. The server\n // owns the grade; it emits the statement.\n complete();\n setSummary('Answer submitted.');\n fireInteraction('submitted', { answers });\n return;\n }\n\n // PRACTICE: unchanged v1 behaviour. `data` carries the key here — exam\n // mode is the only path that may be handed a redact() projection.\n const scoringResult = score('fill-in-the-blanks', data as FillInTheBlanksData, response);\n complete();\n const timeSpent = getTimeSpent();\n const xapiStatement = xAPIBuilder.buildAnsweredStatement({\n actor: ANONYMOUS_ACTOR,\n object: {\n id: objectIdFor(data.id),\n name: { [data.locale ?? 'en-US']: data.title },\n type: 'http://adlnet.gov/expapi/activities/cmi.interaction',\n interactionType: 'fill-in',\n correctResponsesPattern: [\n data.blanks.map((blank) => blank.acceptedAnswers[0] ?? '').join('[,]'),\n ],\n },\n scoringResult,\n timeSpentMs: timeSpent,\n response: JSON.stringify(answers),\n });\n setResult(scoringResult);\n onComplete?.({\n score: scoringResult.score,\n maxScore: scoringResult.maxScore,\n passed: scoringResult.passed,\n timeSpent,\n xapiStatement,\n });\n // Core selects the authored overall feedback on `passed` (B3 fix).\n const overall = scoringResult.feedback;\n setSummary(\n `Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${\n scoringResult.passed ? 'Passed.' : 'Not passed.'\n }${overall ? ` ${overall}` : ''}`,\n );\n fireInteraction('submitted', { answers, score: scoringResult.score });\n };\n\n const correctByBlank = useMemo(() => {\n if (isExam) {\n // Nothing is ever marked client-side in an exam.\n return new Map<string, boolean>();\n }\n if (isReview) {\n // Correctness comes ONLY from the server-computed outcome; the component\n // never scores in review, so an absent/ungraded outcome marks nothing.\n return outcome?.status === 'scored'\n ? correctnessByItem(outcome.details)\n : new Map<string, boolean>();\n }\n return correctnessByItem(result?.details ?? []);\n }, [isExam, isReview, outcome, result]);\n\n /** Review has no local score: its summary is read off `outcome`, or nothing. */\n const reviewSummary = useMemo(() => {\n if (!isReview || outcome === undefined) {\n return null;\n }\n if (outcome.status === 'scored') {\n return `Score ${Math.round(outcome.score * 100)}%. ${\n outcome.passed ? 'Passed.' : 'Not passed.'\n }${outcome.feedback ? ` ${outcome.feedback}` : ''}`;\n }\n return outcome.status === 'deferred' ? 'Not graded yet.' : 'No grade available.';\n }, [isReview, outcome]);\n\n return (\n <form\n className=\"lk-fib\"\n aria-label={data.title}\n lang={locale}\n data-render-mode={renderMode}\n style={theme as CSSProperties | undefined}\n onSubmit={handleSubmit}\n >\n {data.media ? <ActivityMedia media={data.media} /> : null}\n <fieldset disabled={inactive}>\n <p className=\"lk-fib-passage\">\n {segments.map((seg, i) => {\n if (seg.kind === 'text') {\n // biome-ignore lint/suspicious/noArrayIndexKey: passage segments are positional and static\n return <span key={`t${i}`}>{seg.value}</span>;\n }\n const blank = blankById.get(seg.id);\n if (!blank) {\n return null;\n }\n // `Renderable` only widens `scoringStrategy` at the type level, so\n // the answer-key fields still LOOK required here while a redact()\n // projection omits them at runtime. Read them defensively, and\n // never at all in exam mode.\n const acceptedAnswers: string[] | undefined = isExam\n ? undefined\n : blank.acceptedAnswers;\n const blankFeedback = isExam ? undefined : blank.feedback;\n const correctness = correctByBlank.get(seg.id);\n const hintId = `${data.id}-hint-${seg.id}`;\n const correctAnswer = acceptedAnswers?.[0];\n if (revealing && showCorrectAnswers && correctness === false && correctAnswer) {\n return (\n <span key={seg.id} className=\"lk-fib-answer\" data-correct=\"false\">\n {correctAnswer}\n </span>\n );\n }\n return (\n <span key={seg.id} className=\"lk-fib-blank\">\n <input\n type=\"text\"\n aria-label={`Fill in blank ${seg.ordinal}`}\n aria-describedby={blank.hint ? hintId : undefined}\n value={answers[seg.id] ?? ''}\n disabled={inactive}\n aria-disabled={inactive || undefined}\n data-correct={\n revealing && correctness !== undefined ? String(correctness) : undefined\n }\n onChange={(e) => handleChange(seg.id, e.target.value)}\n />\n {/*\n Hints stay available in exam mode by design: lk-core's field\n policy classifies `blanks.hint` as `public`, so it survives\n redact() and is not part of the answer key.\n */}\n {blank.hint ? (\n <>\n {/*\n Default affordance is an icon; the accessible NAME stays\n text via aria-label (screen readers + tests rely on it).\n The glyph is restylable/replaceable via .lk-fib-hint-btn.\n */}\n <button\n type=\"button\"\n className=\"lk-fib-hint-btn\"\n aria-controls={hintId}\n aria-expanded={revealed.has(seg.id)}\n aria-label={revealed.has(seg.id) ? 'Hide hint' : 'Show hint'}\n disabled={inactive}\n onClick={() => toggleHint(seg.id)}\n >\n <svg\n className=\"lk-fib-hint-icon\"\n viewBox=\"0 0 24 24\"\n width=\"16\"\n height=\"16\"\n aria-hidden=\"true\"\n focusable=\"false\"\n >\n <path\n fill=\"currentColor\"\n d=\"M9 21h6v-1H9v1Zm3-19a7 7 0 0 0-4 12.74V17h8v-2.26A7 7 0 0 0 12 2Z\"\n />\n </svg>\n </button>\n {/*\n Not role=\"tooltip\": a real ARIA tooltip is a named\n hover/focus popup with strict WCAG 1.4.13 constraints.\n This is an accessible click-to-toggle disclosure that is\n the input's aria-describedby target and is announced via\n aria-live; the skin gives it a tooltip-like *visual*\n without the tooltip *semantics*. (Refines Task 15.3.)\n */}\n <span id={hintId} aria-live=\"polite\">\n {revealed.has(seg.id) ? blank.hint : ''}\n </span>\n </>\n ) : null}\n {revealing && !feedbackHidden && blankFeedback ? (\n <span\n className=\"lk-fib-blank-feedback\"\n role=\"note\"\n data-correct={correctness !== undefined ? String(correctness) : undefined}\n >\n {blankFeedback}\n </span>\n ) : null}\n </span>\n );\n })}\n </p>\n {/* Review is read-only: there is nothing to submit. */}\n {isReview ? null : (\n <button type=\"submit\" disabled={inactive}>\n {isExam ? 'Submit answers' : 'Check answers'}\n </button>\n )}\n </fieldset>\n {revealing && data.blanks.some((b) => b.feedback) ? (\n <button\n type=\"button\"\n className=\"lk-fib-feedback-toggle\"\n aria-expanded={!feedbackHidden}\n onClick={() => setFeedbackHidden((h) => !h)}\n >\n {feedbackHidden ? 'Show feedback' : 'Hide feedback'}\n </button>\n ) : null}\n <FeedbackRegion id={`${data.id}-feedback`}>\n {isReview ? reviewSummary : summary}\n </FeedbackRegion>\n </form>\n );\n}\n","'use client';\n\nimport { ActivityErrorBoundary } from '../ActivityErrorBoundary.js';\nimport {\n FillInTheBlanks as FillInTheBlanksCore,\n type FillInTheBlanksProps,\n} from './FillInTheBlanks.js';\n\nexport type { FillInTheBlanksProps } from './FillInTheBlanks.js';\n\n/**\n * Public Fill-in-the-Blanks activity: the core wrapped in\n * `ActivityErrorBoundary` so a render failure (incl. the dev schema-validation\n * throw) degrades to an accessible fallback instead of crashing the host.\n */\nexport function FillInTheBlanks(props: FillInTheBlanksProps) {\n return (\n <ActivityErrorBoundary activityTitle={props.data?.title}>\n <FillInTheBlanksCore {...props} />\n </ActivityErrorBoundary>\n );\n}\n"]}
@@ -0,0 +1,259 @@
1
+ 'use strict';
2
+
3
+ var chunkQHOV2A6M_cjs = require('./chunk-QHOV2A6M.cjs');
4
+ var chunkHD2AIQRD_cjs = require('./chunk-HD2AIQRD.cjs');
5
+ var lkCore = require('@intellectif/lk-core');
6
+ var react = require('react');
7
+ var jsxRuntime = require('react/jsx-runtime');
8
+
9
+ function hashSeed(input) {
10
+ let h = 2166136261 >>> 0;
11
+ for (let i = 0; i < input.length; i += 1) {
12
+ h ^= input.charCodeAt(i);
13
+ h = Math.imul(h, 16777619) >>> 0;
14
+ }
15
+ return h;
16
+ }
17
+ function seededShuffle(items, seed) {
18
+ const out = [...items];
19
+ let s = seed >>> 0 || 1;
20
+ for (let i = out.length - 1; i > 0; i -= 1) {
21
+ s = Math.imul(s, 1664525) + 1013904223 >>> 0;
22
+ const j = s % (i + 1);
23
+ [out[i], out[j]] = [out[j], out[i]];
24
+ }
25
+ return out;
26
+ }
27
+ function selectionOf(response) {
28
+ return response?.type === "multiple-choice" ? response.selectedOptionIds : [];
29
+ }
30
+ function isAnswerOption(detail, wasSelected) {
31
+ switch (detail.outcome) {
32
+ case "correct":
33
+ case "incorrect-omission":
34
+ return true;
35
+ case "incorrect":
36
+ case "correct-omission":
37
+ return false;
38
+ default:
39
+ return wasSelected ? detail.correct : !detail.correct;
40
+ }
41
+ }
42
+ function reviewAnnouncement(outcome) {
43
+ if (outcome === void 0) {
44
+ return null;
45
+ }
46
+ if (outcome.status === "scored") {
47
+ const overall = outcome.feedback;
48
+ return `Score ${Math.round(outcome.score * 100)}%. ${outcome.passed ? "Passed." : "Not passed."}${overall ? ` ${overall}` : ""}`;
49
+ }
50
+ if (outcome.status === "deferred") {
51
+ return "Not graded yet.";
52
+ }
53
+ return null;
54
+ }
55
+ function MultipleChoice({
56
+ data,
57
+ onComplete,
58
+ onSubmit,
59
+ value,
60
+ defaultValue,
61
+ onChange,
62
+ renderMode = "practice",
63
+ outcome,
64
+ sanitizeHtml,
65
+ onInteraction,
66
+ theme,
67
+ locale,
68
+ disabled,
69
+ shuffleSeed
70
+ }) {
71
+ const devError = react.useMemo(() => {
72
+ if (!chunkQHOV2A6M_cjs.isDevelopment()) {
73
+ return null;
74
+ }
75
+ if (data.redacted === true) {
76
+ return null;
77
+ }
78
+ const result = lkCore.validateActivity("multiple-choice", data);
79
+ return result.success ? null : new lkCore.ActivitySchemaError("multiple-choice", result.errors);
80
+ }, [data]);
81
+ const sessionIdRef = react.useRef(null);
82
+ const { state, start, complete, getTimeSpent, reset } = chunkHD2AIQRD_cjs.useActivityState();
83
+ const [internalSelection, setInternalSelection] = react.useState(
84
+ () => selectionOf(defaultValue)
85
+ );
86
+ const [summary, setSummary] = react.useState(null);
87
+ const lastDataRef = react.useRef(data);
88
+ react.useEffect(() => {
89
+ if (lastDataRef.current === data) {
90
+ return;
91
+ }
92
+ lastDataRef.current = data;
93
+ setInternalSelection([]);
94
+ setSummary(null);
95
+ reset();
96
+ }, [data, reset]);
97
+ const displayedOptions = react.useMemo(() => {
98
+ if (!data.shuffle) {
99
+ return [...data.options];
100
+ }
101
+ const seedSource = shuffleSeed ?? (sessionIdRef.current ??= chunkQHOV2A6M_cjs.randomSessionId());
102
+ return seededShuffle(data.options, hashSeed(`${seedSource}:${data.id}`));
103
+ }, [data, shuffleSeed]);
104
+ const reviewDetails = react.useMemo(() => {
105
+ if (renderMode !== "review" || outcome === void 0 || outcome.status !== "scored") {
106
+ return null;
107
+ }
108
+ return new Map(outcome.details.map((detail) => [detail.itemId, detail]));
109
+ }, [renderMode, outcome]);
110
+ if (devError) {
111
+ throw devError;
112
+ }
113
+ if (data.redacted === true && renderMode === "practice") {
114
+ throw new Error(
115
+ `Multiple Choice "${data.id}" received redacted activity data in renderMode "practice", which grades locally and has no answer key to grade against. Render redacted data with renderMode="exam" (server grades) or "review" (pass \`outcome\`).`
116
+ );
117
+ }
118
+ const isSingle = data.mode === "single";
119
+ const isExam = renderMode === "exam";
120
+ const isReview = renderMode === "review";
121
+ const submitted = state === "completed";
122
+ const inactive = disabled === true || submitted || isReview;
123
+ const isControlled = value !== void 0;
124
+ const selected = isControlled ? selectionOf(value) : internalSelection;
125
+ const reveal = isReview || renderMode === "practice" && submitted;
126
+ const fireInteraction = (type, payload) => {
127
+ onInteraction?.({ type, activityId: data.id, timestamp: Date.now(), payload });
128
+ };
129
+ const emitChange = (selectedOptionIds) => {
130
+ if (!isControlled) {
131
+ setInternalSelection(selectedOptionIds);
132
+ }
133
+ onChange?.({ type: "multiple-choice", selectedOptionIds });
134
+ };
135
+ const selectSingle = (optionId) => {
136
+ if (inactive) {
137
+ return;
138
+ }
139
+ if (state === "idle") {
140
+ start();
141
+ }
142
+ emitChange([optionId]);
143
+ fireInteraction("option-selected", { optionId });
144
+ };
145
+ const toggleMulti = (optionId, checked) => {
146
+ if (inactive) {
147
+ return;
148
+ }
149
+ if (state === "idle") {
150
+ start();
151
+ }
152
+ emitChange(checked ? [...selected, optionId] : selected.filter((id) => id !== optionId));
153
+ fireInteraction(checked ? "option-selected" : "option-deselected", { optionId });
154
+ };
155
+ const handleSubmit = (event) => {
156
+ event.preventDefault();
157
+ if (inactive) {
158
+ return;
159
+ }
160
+ const response = {
161
+ type: "multiple-choice",
162
+ selectedOptionIds: selected
163
+ };
164
+ onSubmit?.(response);
165
+ if (isExam) {
166
+ complete();
167
+ setSummary("Answer submitted.");
168
+ fireInteraction("submitted", { selectedOptionIds: selected });
169
+ return;
170
+ }
171
+ const scoringResult = lkCore.score("multiple-choice", data, response);
172
+ complete();
173
+ const timeSpent = getTimeSpent();
174
+ const xapiStatement = lkCore.xAPIBuilder.buildAnsweredStatement({
175
+ actor: chunkQHOV2A6M_cjs.ANONYMOUS_ACTOR,
176
+ object: {
177
+ id: chunkQHOV2A6M_cjs.objectIdFor(data.id),
178
+ name: { [data.locale ?? "en-US"]: data.title },
179
+ type: "http://adlnet.gov/expapi/activities/cmi.interaction",
180
+ interactionType: "choice",
181
+ correctResponsesPattern: [
182
+ data.options.filter((option) => option.isCorrect).map((option) => option.id).join("[,]")
183
+ ],
184
+ choices: data.options.map((option) => ({
185
+ id: option.id,
186
+ description: { [data.locale ?? "en-US"]: option.text }
187
+ }))
188
+ },
189
+ scoringResult,
190
+ timeSpentMs: timeSpent,
191
+ response: selected.join(",")
192
+ });
193
+ onComplete?.({
194
+ score: scoringResult.score,
195
+ maxScore: scoringResult.maxScore,
196
+ passed: scoringResult.passed,
197
+ timeSpent,
198
+ xapiStatement
199
+ });
200
+ const overall = scoringResult.feedback;
201
+ setSummary(
202
+ `Answer submitted. Score ${Math.round(scoringResult.score * 100)}%. ${scoringResult.passed ? "Passed." : "Not passed."}${overall ? ` ${overall}` : ""}`
203
+ );
204
+ fireInteraction("submitted", {
205
+ selectedOptionIds: selected,
206
+ score: scoringResult.score
207
+ });
208
+ };
209
+ const questionId = `${data.id}-question`;
210
+ const questionHtml = sanitizeHtml !== void 0 && data.questionHtml !== void 0 ? sanitizeHtml(data.questionHtml) : null;
211
+ const optionList = displayedOptions.map((option) => {
212
+ const checked = selected.includes(option.id);
213
+ let correctness;
214
+ if (isReview) {
215
+ const detail = reviewDetails?.get(option.id);
216
+ correctness = detail === void 0 ? void 0 : String(isAnswerOption(detail, checked));
217
+ } else if (reveal) {
218
+ correctness = String(option.isCorrect);
219
+ }
220
+ return /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "lk-mc-option", "data-correct": correctness, children: [
221
+ /* @__PURE__ */ jsxRuntime.jsx(
222
+ "input",
223
+ {
224
+ type: isSingle ? "radio" : "checkbox",
225
+ name: isSingle ? `${data.id}-options` : void 0,
226
+ value: option.id,
227
+ checked,
228
+ disabled: inactive,
229
+ "aria-disabled": inactive || void 0,
230
+ onChange: (e) => isSingle ? selectSingle(option.id) : toggleMulti(option.id, e.target.checked)
231
+ }
232
+ ),
233
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: option.text }),
234
+ reveal && option.feedback ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "lk-mc-option-feedback", role: "note", children: option.feedback }) : null
235
+ ] }, option.id);
236
+ });
237
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "lk-mc", lang: locale, style: theme, children: [
238
+ data.media ? /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.ActivityMedia, { media: data.media }) : null,
239
+ /* @__PURE__ */ jsxRuntime.jsx("form", { onSubmit: handleSubmit, children: /* @__PURE__ */ jsxRuntime.jsxs("fieldset", { disabled: inactive, children: [
240
+ questionHtml === null ? /* @__PURE__ */ jsxRuntime.jsx("legend", { id: questionId, children: data.question }) : /* @__PURE__ */ jsxRuntime.jsx(
241
+ "legend",
242
+ {
243
+ id: questionId,
244
+ dangerouslySetInnerHTML: { __html: questionHtml }
245
+ }
246
+ ),
247
+ isSingle ? /* @__PURE__ */ jsxRuntime.jsx("div", { role: "radiogroup", "aria-labelledby": questionId, children: optionList }) : /* @__PURE__ */ jsxRuntime.jsx("div", { children: optionList }),
248
+ isReview ? null : /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", disabled: inactive, children: "Submit" })
249
+ ] }) }),
250
+ /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.FeedbackRegion, { id: `${data.id}-feedback`, children: isReview ? reviewAnnouncement(outcome) : summary })
251
+ ] });
252
+ }
253
+ function MultipleChoice2(props) {
254
+ return /* @__PURE__ */ jsxRuntime.jsx(chunkQHOV2A6M_cjs.ActivityErrorBoundary, { activityTitle: props.data?.title, children: /* @__PURE__ */ jsxRuntime.jsx(MultipleChoice, { ...props }) });
255
+ }
256
+
257
+ exports.MultipleChoice = MultipleChoice2;
258
+ //# sourceMappingURL=chunk-W6BH2YMZ.cjs.map
259
+ //# sourceMappingURL=chunk-W6BH2YMZ.cjs.map