react-matchings 0.0.7 → 0.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/README.md CHANGED
@@ -1,559 +1,251 @@
1
- # react-matchings
2
-
3
- A React component for interactive question-answer matching. Create engaging quiz and assessment interfaces where users can drag and drop connections between questions and answers.
4
-
5
- ## Features
6
-
7
- - 🎯 **Drag and Drop Interface** - Intuitive drag-and-drop matching experience
8
- - 🎨 **Highly Customizable** - Customize colors, styling, and behavior through props
9
- - ♿ **Accessible** - Built with accessibility in mind
10
- - 🎭 **Dynamic Styling** - Apply conditional styles based on component state
11
- - 📱 **Responsive** - Works across different screen sizes
12
- - 🎪 **Interactive Feedback** - Visual feedback during dragging and matching
13
-
14
- ## Installation
15
-
16
- ```bash
17
- npm install react-matchings
18
- ```
19
-
20
- ```bash
21
- yarn add react-matchings
22
- ```
23
-
24
- ```bash
25
- pnpm add react-matchings
26
- ```
27
-
28
- ## Basic Usage
29
-
30
- First, import the component, its types, and CSS:
31
-
32
- ```tsx
33
- import { Matching, type TMatch } from "react-matchings";
34
- import "react-matchings/dist/index.css";
35
-
36
- function App() {
37
- const questions = [
38
- { id: 1, text: "What is React?" },
39
- { id: 2, text: "What is TypeScript?" },
40
- { id: 3, text: "What is JavaScript?" },
41
- ];
42
-
43
- const answers = [
44
- { id: 1, text: "A JavaScript library for building UIs" },
45
- { id: 2, text: "A typed superset of JavaScript" },
46
- { id: 3, text: "A programming language" },
47
- ];
48
-
49
- const handleMatchChange = (matches: TMatch[]): void => {
50
- console.log("Current matches:", matches);
51
- // matches format: [{ questionId: 1, answerId: 1 }, ...]
52
- };
53
-
54
- return (
55
- <Matching
56
- questions={questions}
57
- answers={answers}
58
- onChange={handleMatchChange}
59
- />
60
- );
61
- }
62
- ```
63
-
64
- ## Props
65
-
66
- ### Required Props
67
-
68
- #### `questions`
69
-
70
- Array of question objects to display on the left side.
71
-
72
- **Type:** `{ id: number; text: string }[]`
73
-
74
- **Example:**
75
-
76
- ```tsx
77
- const questions = [
78
- { id: 1, text: "Capital of France?" },
79
- { id: 2, text: "Capital of Japan?" },
80
- { id: 3, text: "Capital of Australia?" },
81
- ];
82
- ```
83
-
84
- ---
85
-
86
- #### `answers`
87
-
88
- Array of answer objects to display on the right side.
89
-
90
- **Type:** `{ id: number; text: string }[]`
91
-
92
- **Example:**
93
-
94
- ```tsx
95
- const answers = [
96
- { id: 1, text: "Paris" },
97
- { id: 2, text: "Tokyo" },
98
- { id: 3, text: "Canberra" },
99
- ];
100
- ```
101
-
102
- **Note:** The number of questions and answers don't need to match. Users can connect any question to any answer.
103
-
104
- ---
105
-
106
- ### Optional Props
107
-
108
- #### `onChange`
109
-
110
- Callback function that is called whenever the matches change.
111
-
112
- **Type:** `(matches: TMatch[]) => void`
113
-
114
- **Default:** `undefined`
115
-
116
- **Note:** Import `TMatch` type from the package: `import { type TMatch } from "react-matchings";`
117
-
118
- **Example:**
119
-
120
- ```tsx
121
- import { Matching, type TMatch } from "react-matchings";
122
-
123
- const handleChange = (matches: TMatch[]): void => {
124
- // matches is an array of connections
125
- // Example: [{ questionId: 1, answerId: 2 }, { questionId: 2, answerId: 1 }]
126
- console.log("Matches updated:", matches);
127
- // Save to state, send to API, etc.
128
- };
129
-
130
- <Matching onChange={handleChange} ... />
131
- ```
132
-
133
- **Note:** This prop is optional. If not provided, the component will still function but won't notify parent components of changes.
134
-
135
- ---
136
-
137
- #### `className`
138
-
139
- Additional CSS classes to apply to the container element.
140
-
141
- **Type:** `string`
142
-
143
- **Default:** `undefined`
144
-
145
- **Example:**
146
-
147
- ```tsx
148
- <Matching
149
- className="my-8 p-6 border border-gray-300 rounded-lg"
150
- questions={questions}
151
- answers={answers}
152
- onChange={handleChange}
153
- />
154
- ```
155
-
156
- ---
157
-
158
- #### `questionClassName`
159
-
160
- Custom CSS classes for question buttons.
161
-
162
- **Type:** `string`
163
-
164
- **Default:** `undefined`
165
-
166
- **Example:**
167
-
168
- ```tsx
169
- <Matching
170
- questionClassName="bg-blue-500 hover:bg-blue-600 text-white font-bold"
171
- questions={questions}
172
- answers={answers}
173
- onChange={handleChange}
174
- />
175
- ```
176
-
177
- ---
178
-
179
- #### `answerClassName`
180
-
181
- Custom CSS classes for answer buttons.
182
-
183
- **Type:** `string`
184
-
185
- **Default:** `undefined`
186
-
187
- **Example:**
188
-
189
- ```tsx
190
- <Matching
191
- answerClassName="bg-purple-500 hover:bg-purple-600 text-white"
192
- questions={questions}
193
- answers={answers}
194
- onChange={handleChange}
195
- />
196
- ```
197
-
198
- ---
199
-
200
- #### `lineColor`
201
-
202
- Color of the connecting lines.
203
-
204
- **Type:** `string`
205
-
206
- **Default:** `"black"`
207
-
208
- **Example:**
209
-
210
- ```tsx
211
- <Matching
212
- lineColor="#3b82f6" // blue
213
- questions={questions}
214
- answers={answers}
215
- onChange={handleChange}
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-linear-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: Assessment with Validation
412
-
413
- ```tsx
414
- import { useState, useMemo } from "react";
415
- import { Matching, type TMatch } from "react-matchings";
416
- import "react-matchings/dist/index.css";
417
-
418
- function AssessmentComponent() {
419
- const questions = [
420
- { id: 1, text: "Primary color" },
421
- { id: 2, text: "Secondary color" },
422
- { id: 3, text: "Tertiary color" },
423
- ];
424
-
425
- const answers = [
426
- { id: 1, text: "Red, Blue, Yellow" },
427
- { id: 2, text: "Orange, Green, Purple" },
428
- { id: 3, text: "Red-Orange, Yellow-Green, Blue-Purple" },
429
- ];
430
-
431
- // Correct answers
432
- const correctMatches: TMatch[] = [
433
- { questionId: 1, answerId: 1 },
434
- { questionId: 2, answerId: 2 },
435
- { questionId: 3, answerId: 3 },
436
- ];
437
-
438
- const [matches, setMatches] = useState<TMatch[]>([]);
439
- const [showResults, setShowResults] = useState<boolean>(false);
440
-
441
- const score = useMemo(() => {
442
- if (!showResults) return null;
443
- const correct = matches.filter((match) =>
444
- correctMatches.some(
445
- (cm) =>
446
- cm.questionId === match.questionId && cm.answerId === match.answerId
447
- )
448
- ).length;
449
- return { correct, total: questions.length };
450
- }, [matches, showResults]);
451
-
452
- return (
453
- <div className="p-8">
454
- <Matching
455
- questions={questions}
456
- answers={answers}
457
- onChange={setMatches}
458
- disabled={showResults}
459
- />
460
-
461
- {showResults && score && (
462
- <div className="mt-6 p-4 bg-gray-100 rounded">
463
- <p className="text-lg font-semibold">
464
- Score: {score.correct} / {score.total}
465
- </p>
466
- </div>
467
- )}
468
-
469
- <button
470
- onClick={() => setShowResults(true)}
471
- disabled={showResults || matches.length < questions.length}
472
- className="mt-4 px-6 py-2 bg-blue-500 text-white rounded"
473
- >
474
- Check Answers
475
- </button>
476
- </div>
477
- );
478
- }
479
- ```
480
-
481
- ## How It Works
482
-
483
- 1. **Drag to Connect**: Click and hold on a question, then drag to an answer and release to create a connection.
484
-
485
- 2. **Remove Connection**: Click on a matched question to remove its connection.
486
-
487
- 3. **Visual Feedback**:
488
-
489
- - Dragging shows a dashed line following your cursor
490
- - Matched items are visually distinguished
491
- - Hovering over answers while dragging highlights them
492
-
493
- 4. **State Management**: The `onChange` callback receives an array of all current matches whenever they change.
494
-
495
- ## Styling
496
-
497
- The component uses Tailwind CSS for styling. Make sure to import the CSS file:
498
-
499
- ```tsx
500
- import "react-matchings/dist/index.css";
501
- ```
502
-
503
- You can override default styles using the className props, or customize the colors using the color props.
504
-
505
- ## TypeScript Support
506
-
507
- This package includes full TypeScript definitions. Import types for type-safe usage:
508
-
509
- ```tsx
510
- import { Matching, type TMatch } from "react-matchings";
511
-
512
- // Use the TMatch type for type safety
513
- const handleChange = (matches: TMatch[]): void => {
514
- // matches is properly typed as TMatch[]
515
- // Each match has questionId: number and answerId: number
516
- matches.forEach((match) => {
517
- console.log(match.questionId, match.answerId);
518
- });
519
- };
520
- ```
521
-
522
- The `TMatch` type is defined as:
523
-
524
- ```tsx
525
- type TMatch = {
526
- questionId: number;
527
- answerId: number;
528
- };
529
- ```
530
-
531
- ## Accessibility
532
-
533
- The component includes accessibility features:
534
-
535
- - Proper ARIA attributes (`aria-pressed`)
536
- - Keyboard-friendly interactions
537
- - Focus management
538
- - Semantic HTML structure
539
-
540
- ## Browser Support
541
-
542
- Works in all modern browsers that support:
543
-
544
- - React 18+
545
- - CSS Grid
546
- - SVG
547
- - Drag and Drop API
548
-
549
- ## License
550
-
551
- MIT
552
-
553
- ## Contributing
554
-
555
- Contributions are welcome! Please feel free to submit a Pull Request.
556
-
557
- ## Author
558
-
559
- 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
+ - Pointer support for mouse, touch, and pen input
11
+ - Automatic scrolling while dragging near a scroll container edge
12
+ - One-to-one answers by default, with optional answer reuse
13
+ - Controlled change callback for saving or validating answers
14
+ - Custom classes for the container, question buttons, and answer buttons
15
+ - Configurable connector line color, endpoint color, radius, and offset
16
+ - Per-match styles for validation feedback
17
+ - Disabled state for submitted or read-only flows
18
+ - TypeScript definitions included
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ npm install react-matchings
24
+ ```
25
+
26
+ ```bash
27
+ yarn add react-matchings
28
+ ```
29
+
30
+ ```bash
31
+ pnpm add react-matchings
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```tsx
37
+ import { Matching, type TMatch } from "react-matchings";
38
+
39
+ function App() {
40
+ const questions = [
41
+ { id: 1, text: "What is React?" },
42
+ { id: 2, text: "What is TypeScript?" },
43
+ { id: 3, text: "What is JavaScript?" },
44
+ ];
45
+
46
+ const answers = [
47
+ { id: 1, text: "A JavaScript library for building user interfaces" },
48
+ { id: 2, text: "A typed superset of JavaScript" },
49
+ { id: 3, text: "A programming language for the web" },
50
+ ];
51
+
52
+ const handleChange = (matches: TMatch[]) => {
53
+ console.log(matches);
54
+ };
55
+
56
+ return (
57
+ <Matching questions={questions} answers={answers} onChange={handleChange} />
58
+ );
59
+ }
60
+ ```
61
+
62
+ ## Props
63
+
64
+ | Prop | Type | Default | Description |
65
+ | ------------------- | -------------------------------- | ----------- | ------------------------------------------------------------ |
66
+ | `questions` | `{ id: number; text: string }[]` | Required | Items rendered in the left column. |
67
+ | `answers` | `{ id: number; text: string }[]` | Required | Items rendered in the right column. |
68
+ | `onChange` | `(matches: TMatch[]) => void` | `undefined` | Called whenever the user creates or removes a match. |
69
+ | `className` | `string` | `undefined` | Additional classes for the root container. |
70
+ | `questionClassName` | `string` | `undefined` | Additional classes for question buttons. |
71
+ | `answerClassName` | `string` | `undefined` | Additional classes for answer buttons. |
72
+ | `lineColor` | `string` | `"black"` | CSS color value for connector lines. |
73
+ | `circleColor` | `string` | `"white"` | CSS color value for connector endpoints. |
74
+ | `circleRadius` | `number` | `8` | Radius of connector endpoints in pixels. |
75
+ | `offset` | `number` | `10` | Distance from button edges to connector endpoints in pixels. |
76
+ | `disabled` | `boolean` | `false` | Prevents users from creating or removing matches. |
77
+ | `allowAnswerReuse` | `boolean` | `false` | Allows multiple questions to connect to the same answer. |
78
+ | `autoScroll` | `boolean \| TAutoScrollOptions` | `true` | Scrolls the nearest overflow container while dragging near an edge. |
79
+ | `getMatchStyles` | `(match: TMatch) => TMatchStyles \| undefined` | `undefined` | Returns connector and item styles for an established match. |
80
+
81
+ ## Types
82
+
83
+ ```tsx
84
+ type TMatch = {
85
+ questionId: number;
86
+ answerId: number;
87
+ };
88
+
89
+ type TMatchStyles = {
90
+ lineColor?: string;
91
+ circleColor?: string;
92
+ questionClassName?: string;
93
+ answerClassName?: string;
94
+ };
95
+
96
+ type TAutoScrollOptions = {
97
+ edgeThreshold?: number;
98
+ maxSpeed?: number;
99
+ };
100
+ ```
101
+
102
+ `onChange` receives the full list of current matches:
103
+
104
+ ```tsx
105
+ const handleChange = (matches: TMatch[]) => {
106
+ // Example: [{ questionId: 1, answerId: 2 }]
107
+ };
108
+ ```
109
+
110
+ ## Styling
111
+
112
+ 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:
113
+
114
+ ```tsx
115
+ <Matching
116
+ questions={questions}
117
+ answers={answers}
118
+ className="max-w-3xl rounded-md border border-gray-200 p-6"
119
+ questionClassName="bg-slate-900 text-white hover:bg-slate-700"
120
+ answerClassName="bg-white text-slate-900 border border-slate-300"
121
+ lineColor="#2563eb"
122
+ circleColor="#dbeafe"
123
+ />
124
+ ```
125
+
126
+ If you use Tailwind CSS in the consuming application, these classes can be regular Tailwind utilities. Standard CSS class names also work.
127
+
128
+ ## Example: Validated Assessment
129
+
130
+ ```tsx
131
+ import { useMemo, useState } from "react";
132
+ import { Matching, type TMatch } from "react-matchings";
133
+
134
+ const questions = [
135
+ { id: 1, text: "Capital of France" },
136
+ { id: 2, text: "Capital of Japan" },
137
+ { id: 3, text: "Capital of Australia" },
138
+ ];
139
+
140
+ const answers = [
141
+ { id: 1, text: "Paris" },
142
+ { id: 2, text: "Tokyo" },
143
+ { id: 3, text: "Canberra" },
144
+ ];
145
+
146
+ const correctMatches: TMatch[] = [
147
+ { questionId: 1, answerId: 1 },
148
+ { questionId: 2, answerId: 2 },
149
+ { questionId: 3, answerId: 3 },
150
+ ];
151
+
152
+ export function Assessment() {
153
+ const [matches, setMatches] = useState<TMatch[]>([]);
154
+ const [submitted, setSubmitted] = useState(false);
155
+
156
+ const score = useMemo(() => {
157
+ return matches.filter((match) =>
158
+ correctMatches.some(
159
+ (correct) =>
160
+ correct.questionId === match.questionId &&
161
+ correct.answerId === match.answerId,
162
+ ),
163
+ ).length;
164
+ }, [matches]);
165
+
166
+ return (
167
+ <section>
168
+ <Matching
169
+ questions={questions}
170
+ answers={answers}
171
+ onChange={setMatches}
172
+ disabled={submitted}
173
+ getMatchStyles={(match) =>
174
+ submitted &&
175
+ !correctMatches.some(
176
+ (correct) =>
177
+ correct.questionId === match.questionId &&
178
+ correct.answerId === match.answerId,
179
+ )
180
+ ? {
181
+ lineColor: "#ef4444",
182
+ circleColor: "#ef4444",
183
+ questionClassName: "bg-red-500",
184
+ answerClassName: "bg-red-500",
185
+ }
186
+ : undefined
187
+ }
188
+ />
189
+
190
+ <button
191
+ type="button"
192
+ onClick={() => setSubmitted(true)}
193
+ disabled={submitted || matches.length < questions.length}
194
+ >
195
+ Submit
196
+ </button>
197
+
198
+ {submitted && (
199
+ <p>
200
+ Score: {score} / {questions.length}
201
+ </p>
202
+ )}
203
+ </section>
204
+ );
205
+ }
206
+ ```
207
+
208
+ ## Behavior
209
+
210
+ - Press and drag from a question to an answer to create a match.
211
+ - Drag near the edge of an overflow container to scroll it.
212
+ - Click a matched question to remove its current match.
213
+ - A question can have one answer at a time.
214
+ - An answer can have one question by default. Connecting it again replaces its previous match.
215
+ - Set `allowAnswerReuse` to `true` to connect an answer to more than one question.
216
+ - `onChange` receives the complete match list after each create or remove action.
217
+
218
+ ## Local Testing
219
+
220
+ From this repository:
221
+
222
+ ```bash
223
+ npm run build
224
+ npm pack
225
+ ```
226
+
227
+ Then install the generated tarball in a test React application:
228
+
229
+ ```bash
230
+ npm install /absolute/path/to/react-matchings/react-matchings-0.0.8.tgz
231
+ ```
232
+
233
+ Import only the component:
234
+
235
+ ```tsx
236
+ import { Matching } from "react-matchings";
237
+ ```
238
+
239
+ No CSS import is required.
240
+
241
+ ## Browser Support
242
+
243
+ The component targets modern React applications and uses standard DOM, CSS Grid, and SVG APIs.
244
+
245
+ ## License
246
+
247
+ MIT
248
+
249
+ ## Author
250
+
251
+ Fares Galal