react-matchings 0.0.6 → 0.0.8
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/LICENSE +21 -21
- package/README.md +215 -610
- package/dist/index.css +2 -2
- package/dist/index.js +13 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +13 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +15 -13
- package/dist/index.css.map +0 -1
- package/src/index.css +0 -1
- package/src/index.ts +0 -3
- package/src/lib/utils.ts +0 -6
- package/src/matching.tsx +0 -266
- package/tsconfig.json +0 -15
- package/tsup.config.ts +0 -13
package/README.md
CHANGED
|
@@ -1,610 +1,215 @@
|
|
|
1
|
-
# react-matchings
|
|
2
|
-
|
|
3
|
-
A React component for
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
```tsx
|
|
33
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
{ id:
|
|
39
|
-
{ id:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
{ id:
|
|
45
|
-
{ id:
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```tsx
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
{
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
/>
|
|
217
|
-
|
|
218
|
-
// Or use CSS color names
|
|
219
|
-
<Matching
|
|
220
|
-
lineColor="rgb(59, 130, 246)"
|
|
221
|
-
questions={questions}
|
|
222
|
-
answers={answers}
|
|
223
|
-
onChange={handleChange}
|
|
224
|
-
/>
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
#### `circleColor`
|
|
230
|
-
|
|
231
|
-
Color of the circles at the ends of the connecting lines.
|
|
232
|
-
|
|
233
|
-
**Type:** `string`
|
|
234
|
-
|
|
235
|
-
**Default:** `"white"`
|
|
236
|
-
|
|
237
|
-
**Example:**
|
|
238
|
-
|
|
239
|
-
```tsx
|
|
240
|
-
<Matching
|
|
241
|
-
circleColor="#f3f4f6" // light gray
|
|
242
|
-
questions={questions}
|
|
243
|
-
answers={answers}
|
|
244
|
-
onChange={handleChange}
|
|
245
|
-
/>
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
---
|
|
249
|
-
|
|
250
|
-
#### `circleRadius`
|
|
251
|
-
|
|
252
|
-
Radius of the circles at the ends of the connecting lines, in pixels.
|
|
253
|
-
|
|
254
|
-
**Type:** `number`
|
|
255
|
-
|
|
256
|
-
**Default:** `8`
|
|
257
|
-
|
|
258
|
-
**Example:**
|
|
259
|
-
|
|
260
|
-
```tsx
|
|
261
|
-
<Matching
|
|
262
|
-
circleRadius={12}
|
|
263
|
-
questions={questions}
|
|
264
|
-
answers={answers}
|
|
265
|
-
onChange={handleChange}
|
|
266
|
-
/>
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
#### `offset`
|
|
272
|
-
|
|
273
|
-
Offset distance from the edges where lines connect, in pixels. Controls how far from the edge the connection points are.
|
|
274
|
-
|
|
275
|
-
**Type:** `number`
|
|
276
|
-
|
|
277
|
-
**Default:** `10`
|
|
278
|
-
|
|
279
|
-
**Example:**
|
|
280
|
-
|
|
281
|
-
```tsx
|
|
282
|
-
<Matching
|
|
283
|
-
offset={15}
|
|
284
|
-
questions={questions}
|
|
285
|
-
answers={answers}
|
|
286
|
-
onChange={handleChange}
|
|
287
|
-
/>
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
---
|
|
291
|
-
|
|
292
|
-
#### `disabled`
|
|
293
|
-
|
|
294
|
-
Whether the component is disabled. When disabled, users cannot create or remove matches.
|
|
295
|
-
|
|
296
|
-
**Type:** `boolean`
|
|
297
|
-
|
|
298
|
-
**Default:** `false`
|
|
299
|
-
|
|
300
|
-
**Example:**
|
|
301
|
-
|
|
302
|
-
```tsx
|
|
303
|
-
const [isDisabled, setIsDisabled] = useState(false);
|
|
304
|
-
|
|
305
|
-
<Matching
|
|
306
|
-
disabled={isDisabled}
|
|
307
|
-
questions={questions}
|
|
308
|
-
answers={answers}
|
|
309
|
-
onChange={handleChange}
|
|
310
|
-
/>
|
|
311
|
-
|
|
312
|
-
<button onClick={() => setIsDisabled(!isDisabled)}>
|
|
313
|
-
{isDisabled ? "Enable" : "Disable"} Matching
|
|
314
|
-
</button>
|
|
315
|
-
```
|
|
316
|
-
|
|
317
|
-
---
|
|
318
|
-
|
|
319
|
-
## Complete Examples
|
|
320
|
-
|
|
321
|
-
### Example 1: Basic Quiz Component
|
|
322
|
-
|
|
323
|
-
```tsx
|
|
324
|
-
import { useState } from "react";
|
|
325
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
326
|
-
import "react-matchings/dist/index.css";
|
|
327
|
-
|
|
328
|
-
function QuizApp() {
|
|
329
|
-
const questions = [
|
|
330
|
-
{ id: 1, text: "What is the capital of France?" },
|
|
331
|
-
{ id: 2, text: "What is the capital of Japan?" },
|
|
332
|
-
{ id: 3, text: "What is the capital of Australia?" },
|
|
333
|
-
];
|
|
334
|
-
|
|
335
|
-
const answers = [
|
|
336
|
-
{ id: 1, text: "Paris" },
|
|
337
|
-
{ id: 2, text: "Tokyo" },
|
|
338
|
-
{ id: 3, text: "Canberra" },
|
|
339
|
-
];
|
|
340
|
-
|
|
341
|
-
const [matches, setMatches] = useState<TMatch[]>([]);
|
|
342
|
-
const [submitted, setSubmitted] = useState<boolean>(false);
|
|
343
|
-
|
|
344
|
-
const handleSubmit = (): void => {
|
|
345
|
-
setSubmitted(true);
|
|
346
|
-
// Check answers, calculate score, etc.
|
|
347
|
-
console.log("Submitted matches:", matches);
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
return (
|
|
351
|
-
<div className="p-8 max-w-4xl mx-auto">
|
|
352
|
-
<h1 className="text-2xl font-bold mb-6">Geography Quiz</h1>
|
|
353
|
-
|
|
354
|
-
<Matching
|
|
355
|
-
questions={questions}
|
|
356
|
-
answers={answers}
|
|
357
|
-
onChange={setMatches}
|
|
358
|
-
disabled={submitted}
|
|
359
|
-
/>
|
|
360
|
-
|
|
361
|
-
<button
|
|
362
|
-
onClick={handleSubmit}
|
|
363
|
-
disabled={submitted || matches.length === 0}
|
|
364
|
-
className="mt-6 px-6 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-400"
|
|
365
|
-
>
|
|
366
|
-
Submit Answers
|
|
367
|
-
</button>
|
|
368
|
-
</div>
|
|
369
|
-
);
|
|
370
|
-
}
|
|
371
|
-
```
|
|
372
|
-
|
|
373
|
-
### Example 2: Custom Styled Component
|
|
374
|
-
|
|
375
|
-
```tsx
|
|
376
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
377
|
-
import "react-matchings/dist/index.css";
|
|
378
|
-
|
|
379
|
-
function StyledMatching() {
|
|
380
|
-
const questions = [
|
|
381
|
-
{ id: 1, text: "Question 1" },
|
|
382
|
-
{ id: 2, text: "Question 2" },
|
|
383
|
-
];
|
|
384
|
-
|
|
385
|
-
const answers = [
|
|
386
|
-
{ id: 1, text: "Answer 1" },
|
|
387
|
-
{ id: 2, text: "Answer 2" },
|
|
388
|
-
];
|
|
389
|
-
|
|
390
|
-
const handleChange = (matches: TMatch[]): void => {
|
|
391
|
-
console.log(matches);
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
return (
|
|
395
|
-
<Matching
|
|
396
|
-
questions={questions}
|
|
397
|
-
answers={answers}
|
|
398
|
-
onChange={handleChange}
|
|
399
|
-
className="p-8 bg-gradient-to-br from-blue-50 to-purple-50 rounded-xl shadow-lg"
|
|
400
|
-
questionClassName="p-4 rounded-lg font-semibold transition-all duration-200 bg-blue-500 text-white hover:bg-blue-600"
|
|
401
|
-
answerClassName="p-4 rounded-lg font-semibold transition-all duration-200 bg-purple-500 text-white hover:bg-purple-600"
|
|
402
|
-
lineColor="#8b5cf6"
|
|
403
|
-
circleColor="#e9d5ff"
|
|
404
|
-
circleRadius={10}
|
|
405
|
-
offset={12}
|
|
406
|
-
/>
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
```
|
|
410
|
-
|
|
411
|
-
### Example 3: With Initial Matches
|
|
412
|
-
|
|
413
|
-
```tsx
|
|
414
|
-
import { useState, useEffect } from "react";
|
|
415
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
416
|
-
import "react-matchings/dist/index.css";
|
|
417
|
-
|
|
418
|
-
function MatchingWithInitialState() {
|
|
419
|
-
const questions = [
|
|
420
|
-
{ id: 1, text: "React" },
|
|
421
|
-
{ id: 2, text: "Vue" },
|
|
422
|
-
{ id: 3, text: "Angular" },
|
|
423
|
-
];
|
|
424
|
-
|
|
425
|
-
const answers = [
|
|
426
|
-
{ id: 1, text: "Facebook" },
|
|
427
|
-
{ id: 2, text: "Evan You" },
|
|
428
|
-
{ id: 3, text: "Google" },
|
|
429
|
-
];
|
|
430
|
-
|
|
431
|
-
const [matches, setMatches] = useState<TMatch[]>([]);
|
|
432
|
-
|
|
433
|
-
// Load initial matches (e.g., from localStorage or API)
|
|
434
|
-
useEffect(() => {
|
|
435
|
-
const savedMatches = localStorage.getItem("matches");
|
|
436
|
-
if (savedMatches) {
|
|
437
|
-
try {
|
|
438
|
-
const parsed: TMatch[] = JSON.parse(savedMatches);
|
|
439
|
-
setMatches(parsed);
|
|
440
|
-
} catch (error) {
|
|
441
|
-
console.error("Failed to parse saved matches:", error);
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}, []);
|
|
445
|
-
|
|
446
|
-
// Save matches when they change
|
|
447
|
-
const handleMatchChange = (newMatches: TMatch[]): void => {
|
|
448
|
-
setMatches(newMatches);
|
|
449
|
-
localStorage.setItem("matches", JSON.stringify(newMatches));
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
return (
|
|
453
|
-
<Matching
|
|
454
|
-
questions={questions}
|
|
455
|
-
answers={answers}
|
|
456
|
-
onChange={handleMatchChange}
|
|
457
|
-
/>
|
|
458
|
-
);
|
|
459
|
-
}
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Example 4: Assessment with Validation
|
|
463
|
-
|
|
464
|
-
```tsx
|
|
465
|
-
import { useState, useMemo } from "react";
|
|
466
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
467
|
-
import "react-matchings/dist/index.css";
|
|
468
|
-
|
|
469
|
-
function AssessmentComponent() {
|
|
470
|
-
const questions = [
|
|
471
|
-
{ id: 1, text: "Primary color" },
|
|
472
|
-
{ id: 2, text: "Secondary color" },
|
|
473
|
-
{ id: 3, text: "Tertiary color" },
|
|
474
|
-
];
|
|
475
|
-
|
|
476
|
-
const answers = [
|
|
477
|
-
{ id: 1, text: "Red, Blue, Yellow" },
|
|
478
|
-
{ id: 2, text: "Orange, Green, Purple" },
|
|
479
|
-
{ id: 3, text: "Red-Orange, Yellow-Green, Blue-Purple" },
|
|
480
|
-
];
|
|
481
|
-
|
|
482
|
-
// Correct answers
|
|
483
|
-
const correctMatches: TMatch[] = [
|
|
484
|
-
{ questionId: 1, answerId: 1 },
|
|
485
|
-
{ questionId: 2, answerId: 2 },
|
|
486
|
-
{ questionId: 3, answerId: 3 },
|
|
487
|
-
];
|
|
488
|
-
|
|
489
|
-
const [matches, setMatches] = useState<TMatch[]>([]);
|
|
490
|
-
const [showResults, setShowResults] = useState<boolean>(false);
|
|
491
|
-
|
|
492
|
-
const score = useMemo(() => {
|
|
493
|
-
if (!showResults) return null;
|
|
494
|
-
const correct = matches.filter((match) =>
|
|
495
|
-
correctMatches.some(
|
|
496
|
-
(cm) =>
|
|
497
|
-
cm.questionId === match.questionId && cm.answerId === match.answerId
|
|
498
|
-
)
|
|
499
|
-
).length;
|
|
500
|
-
return { correct, total: questions.length };
|
|
501
|
-
}, [matches, showResults]);
|
|
502
|
-
|
|
503
|
-
return (
|
|
504
|
-
<div className="p-8">
|
|
505
|
-
<Matching
|
|
506
|
-
questions={questions}
|
|
507
|
-
answers={answers}
|
|
508
|
-
onChange={setMatches}
|
|
509
|
-
disabled={showResults}
|
|
510
|
-
/>
|
|
511
|
-
|
|
512
|
-
{showResults && score && (
|
|
513
|
-
<div className="mt-6 p-4 bg-gray-100 rounded">
|
|
514
|
-
<p className="text-lg font-semibold">
|
|
515
|
-
Score: {score.correct} / {score.total}
|
|
516
|
-
</p>
|
|
517
|
-
</div>
|
|
518
|
-
)}
|
|
519
|
-
|
|
520
|
-
<button
|
|
521
|
-
onClick={() => setShowResults(true)}
|
|
522
|
-
disabled={showResults || matches.length < questions.length}
|
|
523
|
-
className="mt-4 px-6 py-2 bg-blue-500 text-white rounded"
|
|
524
|
-
>
|
|
525
|
-
Check Answers
|
|
526
|
-
</button>
|
|
527
|
-
</div>
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
```
|
|
531
|
-
|
|
532
|
-
## How It Works
|
|
533
|
-
|
|
534
|
-
1. **Drag to Connect**: Click and hold on a question, then drag to an answer and release to create a connection.
|
|
535
|
-
|
|
536
|
-
2. **Remove Connection**: Click on a matched question to remove its connection.
|
|
537
|
-
|
|
538
|
-
3. **Visual Feedback**:
|
|
539
|
-
|
|
540
|
-
- Dragging shows a dashed line following your cursor
|
|
541
|
-
- Matched items are visually distinguished
|
|
542
|
-
- Hovering over answers while dragging highlights them
|
|
543
|
-
|
|
544
|
-
4. **State Management**: The `onChange` callback receives an array of all current matches whenever they change.
|
|
545
|
-
|
|
546
|
-
## Styling
|
|
547
|
-
|
|
548
|
-
The component uses Tailwind CSS for styling. Make sure to import the CSS file:
|
|
549
|
-
|
|
550
|
-
```tsx
|
|
551
|
-
import "react-matchings/dist/index.css";
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
You can override default styles using the className props, or customize the colors using the color props.
|
|
555
|
-
|
|
556
|
-
## TypeScript Support
|
|
557
|
-
|
|
558
|
-
This package includes full TypeScript definitions. Import types for type-safe usage:
|
|
559
|
-
|
|
560
|
-
```tsx
|
|
561
|
-
import { Matching, type TMatch } from "react-matchings";
|
|
562
|
-
|
|
563
|
-
// Use the TMatch type for type safety
|
|
564
|
-
const handleChange = (matches: TMatch[]): void => {
|
|
565
|
-
// matches is properly typed as TMatch[]
|
|
566
|
-
// Each match has questionId: number and answerId: number
|
|
567
|
-
matches.forEach((match) => {
|
|
568
|
-
console.log(match.questionId, match.answerId);
|
|
569
|
-
});
|
|
570
|
-
};
|
|
571
|
-
```
|
|
572
|
-
|
|
573
|
-
The `TMatch` type is defined as:
|
|
574
|
-
|
|
575
|
-
```tsx
|
|
576
|
-
type TMatch = {
|
|
577
|
-
questionId: number;
|
|
578
|
-
answerId: number;
|
|
579
|
-
};
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
## Accessibility
|
|
583
|
-
|
|
584
|
-
The component includes accessibility features:
|
|
585
|
-
|
|
586
|
-
- Proper ARIA attributes (`aria-pressed`)
|
|
587
|
-
- Keyboard-friendly interactions
|
|
588
|
-
- Focus management
|
|
589
|
-
- Semantic HTML structure
|
|
590
|
-
|
|
591
|
-
## Browser Support
|
|
592
|
-
|
|
593
|
-
Works in all modern browsers that support:
|
|
594
|
-
|
|
595
|
-
- React 18+
|
|
596
|
-
- CSS Grid
|
|
597
|
-
- SVG
|
|
598
|
-
- Drag and Drop API
|
|
599
|
-
|
|
600
|
-
## License
|
|
601
|
-
|
|
602
|
-
MIT
|
|
603
|
-
|
|
604
|
-
## Contributing
|
|
605
|
-
|
|
606
|
-
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
607
|
-
|
|
608
|
-
## Author
|
|
609
|
-
|
|
610
|
-
Fares Galal
|
|
1
|
+
# react-matchings
|
|
2
|
+
|
|
3
|
+
A React component for building question-and-answer matching interactions. It renders two columns of items and lets users connect questions to answers by dragging between them.
|
|
4
|
+
|
|
5
|
+
Default styles are included automatically when the component is imported. Consumers do not need to import a separate CSS file.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Drag-to-connect matching interaction
|
|
10
|
+
- Controlled change callback for saving or validating answers
|
|
11
|
+
- Custom classes for the container, question buttons, and answer buttons
|
|
12
|
+
- Configurable connector line color, endpoint color, radius, and offset
|
|
13
|
+
- Disabled state for submitted or read-only flows
|
|
14
|
+
- TypeScript definitions included
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install react-matchings
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
yarn add react-matchings
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pnpm add react-matchings
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { Matching, type TMatch } from "react-matchings";
|
|
34
|
+
|
|
35
|
+
function App() {
|
|
36
|
+
const questions = [
|
|
37
|
+
{ id: 1, text: "What is React?" },
|
|
38
|
+
{ id: 2, text: "What is TypeScript?" },
|
|
39
|
+
{ id: 3, text: "What is JavaScript?" },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const answers = [
|
|
43
|
+
{ id: 1, text: "A JavaScript library for building user interfaces" },
|
|
44
|
+
{ id: 2, text: "A typed superset of JavaScript" },
|
|
45
|
+
{ id: 3, text: "A programming language for the web" },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const handleChange = (matches: TMatch[]) => {
|
|
49
|
+
console.log(matches);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Matching questions={questions} answers={answers} onChange={handleChange} />
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Props
|
|
59
|
+
|
|
60
|
+
| Prop | Type | Default | Description |
|
|
61
|
+
| ------------------- | -------------------------------- | ----------- | ------------------------------------------------------------ |
|
|
62
|
+
| `questions` | `{ id: number; text: string }[]` | Required | Items rendered in the left column. |
|
|
63
|
+
| `answers` | `{ id: number; text: string }[]` | Required | Items rendered in the right column. |
|
|
64
|
+
| `onChange` | `(matches: TMatch[]) => void` | `undefined` | Called whenever the user creates or removes a match. |
|
|
65
|
+
| `className` | `string` | `undefined` | Additional classes for the root container. |
|
|
66
|
+
| `questionClassName` | `string` | `undefined` | Additional classes for question buttons. |
|
|
67
|
+
| `answerClassName` | `string` | `undefined` | Additional classes for answer buttons. |
|
|
68
|
+
| `lineColor` | `string` | `"black"` | CSS color value for connector lines. |
|
|
69
|
+
| `circleColor` | `string` | `"white"` | CSS color value for connector endpoints. |
|
|
70
|
+
| `circleRadius` | `number` | `8` | Radius of connector endpoints in pixels. |
|
|
71
|
+
| `offset` | `number` | `10` | Distance from button edges to connector endpoints in pixels. |
|
|
72
|
+
| `disabled` | `boolean` | `false` | Prevents users from creating or removing matches. |
|
|
73
|
+
|
|
74
|
+
## Types
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
type TMatch = {
|
|
78
|
+
questionId: number;
|
|
79
|
+
answerId: number;
|
|
80
|
+
};
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`onChange` receives the full list of current matches:
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
const handleChange = (matches: TMatch[]) => {
|
|
87
|
+
// Example: [{ questionId: 1, answerId: 2 }]
|
|
88
|
+
};
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Styling
|
|
92
|
+
|
|
93
|
+
The component ships with default styles and injects them automatically in the browser. You can override the default button and layout styles with class props:
|
|
94
|
+
|
|
95
|
+
```tsx
|
|
96
|
+
<Matching
|
|
97
|
+
questions={questions}
|
|
98
|
+
answers={answers}
|
|
99
|
+
className="max-w-3xl rounded-md border border-gray-200 p-6"
|
|
100
|
+
questionClassName="bg-slate-900 text-white hover:bg-slate-700"
|
|
101
|
+
answerClassName="bg-white text-slate-900 border border-slate-300"
|
|
102
|
+
lineColor="#2563eb"
|
|
103
|
+
circleColor="#dbeafe"
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
If you use Tailwind CSS in the consuming application, these classes can be regular Tailwind utilities. Standard CSS class names also work.
|
|
108
|
+
|
|
109
|
+
## Example: Validated Assessment
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { useMemo, useState } from "react";
|
|
113
|
+
import { Matching, type TMatch } from "react-matchings";
|
|
114
|
+
|
|
115
|
+
const questions = [
|
|
116
|
+
{ id: 1, text: "Capital of France" },
|
|
117
|
+
{ id: 2, text: "Capital of Japan" },
|
|
118
|
+
{ id: 3, text: "Capital of Australia" },
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
const answers = [
|
|
122
|
+
{ id: 1, text: "Paris" },
|
|
123
|
+
{ id: 2, text: "Tokyo" },
|
|
124
|
+
{ id: 3, text: "Canberra" },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
const correctMatches: TMatch[] = [
|
|
128
|
+
{ questionId: 1, answerId: 1 },
|
|
129
|
+
{ questionId: 2, answerId: 2 },
|
|
130
|
+
{ questionId: 3, answerId: 3 },
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
export function Assessment() {
|
|
134
|
+
const [matches, setMatches] = useState<TMatch[]>([]);
|
|
135
|
+
const [submitted, setSubmitted] = useState(false);
|
|
136
|
+
|
|
137
|
+
const score = useMemo(() => {
|
|
138
|
+
return matches.filter((match) =>
|
|
139
|
+
correctMatches.some(
|
|
140
|
+
(correct) =>
|
|
141
|
+
correct.questionId === match.questionId &&
|
|
142
|
+
correct.answerId === match.answerId,
|
|
143
|
+
),
|
|
144
|
+
).length;
|
|
145
|
+
}, [matches]);
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<section>
|
|
149
|
+
<Matching
|
|
150
|
+
questions={questions}
|
|
151
|
+
answers={answers}
|
|
152
|
+
onChange={setMatches}
|
|
153
|
+
disabled={submitted}
|
|
154
|
+
/>
|
|
155
|
+
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
onClick={() => setSubmitted(true)}
|
|
159
|
+
disabled={submitted || matches.length < questions.length}
|
|
160
|
+
>
|
|
161
|
+
Submit
|
|
162
|
+
</button>
|
|
163
|
+
|
|
164
|
+
{submitted && (
|
|
165
|
+
<p>
|
|
166
|
+
Score: {score} / {questions.length}
|
|
167
|
+
</p>
|
|
168
|
+
)}
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Behavior
|
|
175
|
+
|
|
176
|
+
- Press and drag from a question to an answer to create a match.
|
|
177
|
+
- Click a matched question to remove its current match.
|
|
178
|
+
- A question can have one answer at a time.
|
|
179
|
+
- An answer can be connected to more than one question.
|
|
180
|
+
- `onChange` receives the complete match list after each create or remove action.
|
|
181
|
+
|
|
182
|
+
## Local Testing
|
|
183
|
+
|
|
184
|
+
From this repository:
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
npm run build
|
|
188
|
+
npm pack
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Then install the generated tarball in a test React application:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
npm install /absolute/path/to/react-matchings/react-matchings-0.0.8.tgz
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Import only the component:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { Matching } from "react-matchings";
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
No CSS import is required.
|
|
204
|
+
|
|
205
|
+
## Browser Support
|
|
206
|
+
|
|
207
|
+
The component targets modern React applications and uses standard DOM, CSS Grid, and SVG APIs.
|
|
208
|
+
|
|
209
|
+
## License
|
|
210
|
+
|
|
211
|
+
MIT
|
|
212
|
+
|
|
213
|
+
## Author
|
|
214
|
+
|
|
215
|
+
Fares Galal
|