@capyx/components-library 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/dist/addons/CharacterCountInput.d.ts +73 -0
- package/dist/addons/CharacterCountInput.d.ts.map +1 -0
- package/dist/addons/CharacterCountInput.js +130 -0
- package/{lib/addons/index.ts → dist/addons/index.d.ts} +1 -0
- package/dist/addons/index.d.ts.map +1 -0
- package/dist/addons/index.js +1 -0
- package/dist/components/CheckInput.d.ts +49 -0
- package/dist/components/CheckInput.d.ts.map +1 -0
- package/dist/components/CheckInput.js +58 -0
- package/dist/components/DateInput.d.ts +63 -0
- package/dist/components/DateInput.d.ts.map +1 -0
- package/dist/components/DateInput.js +86 -0
- package/dist/components/FileInput.d.ts +102 -0
- package/dist/components/FileInput.d.ts.map +1 -0
- package/dist/components/FileInput.js +164 -0
- package/dist/components/RichTextInput.d.ts +34 -0
- package/dist/components/RichTextInput.d.ts.map +1 -0
- package/dist/components/RichTextInput.js +57 -0
- package/dist/components/SelectInput.d.ts +54 -0
- package/dist/components/SelectInput.d.ts.map +1 -0
- package/dist/components/SelectInput.js +64 -0
- package/dist/components/SwitchInput.d.ts +46 -0
- package/dist/components/SwitchInput.d.ts.map +1 -0
- package/dist/components/SwitchInput.js +53 -0
- package/dist/components/TagsInput.d.ts +35 -0
- package/dist/components/TagsInput.d.ts.map +1 -0
- package/dist/components/TagsInput.js +67 -0
- package/dist/components/TextAreaInput.d.ts +71 -0
- package/dist/components/TextAreaInput.d.ts.map +1 -0
- package/dist/components/TextAreaInput.js +113 -0
- package/dist/components/TextInput.d.ts +89 -0
- package/dist/components/TextInput.d.ts.map +1 -0
- package/dist/components/TextInput.js +177 -0
- package/dist/components/index.d.ts +10 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/index.cjs +18 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/package.json +85 -70
- package/.storybook/main.ts +0 -33
- package/.storybook/preview.ts +0 -36
- package/.storybook/vitest.setup.ts +0 -7
- package/biome.json +0 -37
- package/lib/addons/CharacterCountInput.tsx +0 -204
- package/lib/components/CheckInput.tsx +0 -126
- package/lib/components/DateInput.tsx +0 -179
- package/lib/components/FileInput.tsx +0 -353
- package/lib/components/RichTextInput.tsx +0 -112
- package/lib/components/SelectInput.tsx +0 -144
- package/lib/components/SwitchInput.tsx +0 -116
- package/lib/components/TagsInput.tsx +0 -118
- package/lib/components/TextAreaInput.tsx +0 -211
- package/lib/components/TextInput.tsx +0 -381
- package/stories/CharacterCountInput.stories.tsx +0 -104
- package/stories/CheckInput.stories.tsx +0 -80
- package/stories/DateInput.stories.tsx +0 -137
- package/stories/FileInput.stories.tsx +0 -125
- package/stories/RichTextInput.stories.tsx +0 -77
- package/stories/SelectInput.stories.tsx +0 -131
- package/stories/SwitchInput.stories.tsx +0 -80
- package/stories/TagsInput.stories.tsx +0 -69
- package/stories/TextAreaInput.stories.tsx +0 -117
- package/stories/TextInput.stories.tsx +0 -167
- package/vitest.config.ts +0 -37
- package/vitest.shims.d.ts +0 -1
- /package/{lib/components/index.ts → dist/components/index.js} +0 -0
- /package/{lib/index.ts → dist/index.js} +0 -0
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
type ChangeEvent,
|
|
3
|
-
type FC,
|
|
4
|
-
type ReactNode,
|
|
5
|
-
useRef,
|
|
6
|
-
useState,
|
|
7
|
-
} from 'react';
|
|
8
|
-
import { Button, Form } from 'react-bootstrap';
|
|
9
|
-
import { Controller, useFormContext } from 'react-hook-form';
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Props for the FileInput component
|
|
13
|
-
*/
|
|
14
|
-
export type FileInputProps = {
|
|
15
|
-
/** The name of the file input field for form integration */
|
|
16
|
-
name?: string;
|
|
17
|
-
/** File types to accept (e.g., "image/*", ".pdf,.doc") */
|
|
18
|
-
accept?: string;
|
|
19
|
-
/** Whether to allow selecting multiple files */
|
|
20
|
-
multiple?: boolean;
|
|
21
|
-
/** Callback fired when file selection changes */
|
|
22
|
-
onChange?: (files: File | File[] | null) => void;
|
|
23
|
-
/** Callback fired when upload process starts */
|
|
24
|
-
onUploadStart?: () => void;
|
|
25
|
-
/** Callback fired when upload completes successfully */
|
|
26
|
-
onUploadComplete?: () => void;
|
|
27
|
-
/** Callback fired when upload encounters an error */
|
|
28
|
-
onUploadError?: (error: Error) => void;
|
|
29
|
-
/** Whether the file input is disabled */
|
|
30
|
-
disabled?: boolean;
|
|
31
|
-
/** Maximum file size in bytes */
|
|
32
|
-
maxSize?: number;
|
|
33
|
-
/** CSS class for the container element */
|
|
34
|
-
containerClassName?: string;
|
|
35
|
-
/** CSS class for the label element */
|
|
36
|
-
labelClassName?: string;
|
|
37
|
-
/** CSS class for the button element */
|
|
38
|
-
buttonClassName?: string;
|
|
39
|
-
/** CSS class for the file info text */
|
|
40
|
-
fileInfoClassName?: string;
|
|
41
|
-
/** CSS class for error messages */
|
|
42
|
-
errorClassName?: string;
|
|
43
|
-
/** Label text or element displayed above the input */
|
|
44
|
-
label?: ReactNode;
|
|
45
|
-
/** Text or element displayed on the button */
|
|
46
|
-
buttonText?: ReactNode;
|
|
47
|
-
/** Text shown when no file is selected */
|
|
48
|
-
noFileText?: ReactNode;
|
|
49
|
-
/** Text shown during file upload */
|
|
50
|
-
uploadingText?: ReactNode;
|
|
51
|
-
/** Custom function to format the file info display */
|
|
52
|
-
formatFileInfo?: (files: File | File[]) => ReactNode;
|
|
53
|
-
/** Custom validation function for individual files */
|
|
54
|
-
validateFile?: (file: File) => { valid: boolean; error?: string };
|
|
55
|
-
/** Whether file selection is required */
|
|
56
|
-
required?: boolean;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* A customizable file input component with validation, upload lifecycle hooks,
|
|
61
|
-
* and react-hook-form integration.
|
|
62
|
-
*
|
|
63
|
-
* Features:
|
|
64
|
-
* - Single or multiple file selection
|
|
65
|
-
* - File type filtering via accept attribute
|
|
66
|
-
* - File size validation with configurable max size
|
|
67
|
-
* - Custom file validation logic
|
|
68
|
-
* - Upload lifecycle callbacks (start, complete, error)
|
|
69
|
-
* - Fully customizable UI with className props
|
|
70
|
-
* - Integration with react-hook-form for form validation
|
|
71
|
-
* - Custom formatting for file information display
|
|
72
|
-
*
|
|
73
|
-
* @example
|
|
74
|
-
* // Basic file upload with react-hook-form
|
|
75
|
-
* <FileInput
|
|
76
|
-
* name="avatar"
|
|
77
|
-
* label="Profile Picture"
|
|
78
|
-
* accept="image/*"
|
|
79
|
-
* required
|
|
80
|
-
* maxSize={5 * 1024 * 1024} // 5MB
|
|
81
|
-
* />
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* // Multiple files with custom validation
|
|
85
|
-
* <FileInput
|
|
86
|
-
* name="documents"
|
|
87
|
-
* label="Upload Documents"
|
|
88
|
-
* multiple
|
|
89
|
-
* accept=".pdf,.doc,.docx"
|
|
90
|
-
* validateFile={(file) => {
|
|
91
|
-
* if (file.name.length > 100) {
|
|
92
|
-
* return { valid: false, error: "Filename too long" };
|
|
93
|
-
* }
|
|
94
|
-
* return { valid: true };
|
|
95
|
-
* }}
|
|
96
|
-
* onChange={(files) => console.log(files)}
|
|
97
|
-
* />
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* // With upload lifecycle hooks
|
|
101
|
-
* <FileInput
|
|
102
|
-
* name="report"
|
|
103
|
-
* onUploadStart={() => console.log("Starting upload...")}
|
|
104
|
-
* onUploadComplete={() => console.log("Upload complete!")}
|
|
105
|
-
* onUploadError={(error) => console.error(error)}
|
|
106
|
-
* />
|
|
107
|
-
*/
|
|
108
|
-
export const FileInput: FC<FileInputProps> = ({
|
|
109
|
-
name,
|
|
110
|
-
accept,
|
|
111
|
-
multiple = false,
|
|
112
|
-
onChange,
|
|
113
|
-
onUploadStart,
|
|
114
|
-
onUploadComplete,
|
|
115
|
-
onUploadError,
|
|
116
|
-
disabled = false,
|
|
117
|
-
maxSize,
|
|
118
|
-
containerClassName = '',
|
|
119
|
-
labelClassName = '',
|
|
120
|
-
buttonClassName = '',
|
|
121
|
-
fileInfoClassName = '',
|
|
122
|
-
errorClassName = '',
|
|
123
|
-
label = 'Upload File',
|
|
124
|
-
buttonText = 'Choose File',
|
|
125
|
-
noFileText = 'No file selected',
|
|
126
|
-
uploadingText = 'Uploading...',
|
|
127
|
-
formatFileInfo,
|
|
128
|
-
validateFile,
|
|
129
|
-
required = false,
|
|
130
|
-
}) => {
|
|
131
|
-
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
132
|
-
const [selectedFiles, setSelectedFiles] = useState<File | File[] | null>(
|
|
133
|
-
null,
|
|
134
|
-
);
|
|
135
|
-
const [isUploading, setIsUploading] = useState(false);
|
|
136
|
-
const [error, setError] = useState<string | null>(null);
|
|
137
|
-
const formContext = useFormContext();
|
|
138
|
-
|
|
139
|
-
// Helper to safely get nested error
|
|
140
|
-
const getFieldError = (fieldName: string) => {
|
|
141
|
-
try {
|
|
142
|
-
const error = formContext?.formState?.errors?.[fieldName];
|
|
143
|
-
return error?.message as string | undefined;
|
|
144
|
-
} catch {
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const formErrorMessage = name ? getFieldError(name) : undefined;
|
|
150
|
-
|
|
151
|
-
const handleFileUploadClick = () => {
|
|
152
|
-
if (isUploading || disabled) return;
|
|
153
|
-
fileInputRef.current?.click();
|
|
154
|
-
};
|
|
155
|
-
|
|
156
|
-
const validateFiles = (files: File[]): { valid: boolean; error?: string } => {
|
|
157
|
-
for (const file of files) {
|
|
158
|
-
// Check file size
|
|
159
|
-
if (maxSize && file.size > maxSize) {
|
|
160
|
-
return {
|
|
161
|
-
valid: false,
|
|
162
|
-
error: `File size exceeds maximum allowed size of ${(maxSize / 1024 / 1024).toFixed(2)}MB`,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Custom validation
|
|
167
|
-
if (validateFile) {
|
|
168
|
-
const result = validateFile(file);
|
|
169
|
-
if (!result.valid) {
|
|
170
|
-
return result;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
return { valid: true };
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const handleFileChange = (
|
|
179
|
-
event: ChangeEvent<HTMLInputElement>,
|
|
180
|
-
fieldOnChange?: (value: File | File[] | null) => void,
|
|
181
|
-
) => {
|
|
182
|
-
setError(null);
|
|
183
|
-
|
|
184
|
-
const files = event.target.files;
|
|
185
|
-
if (!files || files.length === 0) {
|
|
186
|
-
setSelectedFiles(null);
|
|
187
|
-
onChange?.(null);
|
|
188
|
-
fieldOnChange?.(null);
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const fileArray = Array.from(files);
|
|
193
|
-
|
|
194
|
-
const validation = validateFiles(fileArray);
|
|
195
|
-
if (!validation.valid) {
|
|
196
|
-
setError(validation.error ?? 'Invalid file');
|
|
197
|
-
if (fileInputRef.current) {
|
|
198
|
-
fileInputRef.current.value = '';
|
|
199
|
-
}
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const result = multiple ? fileArray : fileArray[0];
|
|
204
|
-
setSelectedFiles(result);
|
|
205
|
-
|
|
206
|
-
// Reset input value to allow re-selecting the same file
|
|
207
|
-
if (fileInputRef.current) {
|
|
208
|
-
fileInputRef.current.value = '';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
onUploadStart?.();
|
|
213
|
-
setIsUploading(true);
|
|
214
|
-
onChange?.(result);
|
|
215
|
-
fieldOnChange?.(result);
|
|
216
|
-
onUploadComplete?.();
|
|
217
|
-
} catch (err) {
|
|
218
|
-
const error = err instanceof Error ? err : new Error('Upload failed');
|
|
219
|
-
setError(error.message);
|
|
220
|
-
onUploadError?.(error);
|
|
221
|
-
} finally {
|
|
222
|
-
setIsUploading(false);
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const renderFileInfo = (): ReactNode => {
|
|
227
|
-
if (isUploading) {
|
|
228
|
-
return (
|
|
229
|
-
<Form.Text className={fileInfoClassName}>{uploadingText}</Form.Text>
|
|
230
|
-
);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (!selectedFiles) {
|
|
234
|
-
return <Form.Text className={fileInfoClassName}>{noFileText}</Form.Text>;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
if (formatFileInfo) {
|
|
238
|
-
return (
|
|
239
|
-
<Form.Text className={fileInfoClassName}>
|
|
240
|
-
{formatFileInfo(selectedFiles)}
|
|
241
|
-
</Form.Text>
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
if (Array.isArray(selectedFiles)) {
|
|
246
|
-
return (
|
|
247
|
-
<Form.Text className={fileInfoClassName}>
|
|
248
|
-
{selectedFiles.length} file{selectedFiles.length !== 1 ? 's' : ''}{' '}
|
|
249
|
-
selected
|
|
250
|
-
</Form.Text>
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
<Form.Text className={fileInfoClassName}>{selectedFiles.name}</Form.Text>
|
|
256
|
-
);
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
const displayError = error || formErrorMessage;
|
|
260
|
-
|
|
261
|
-
// Render with form context integration
|
|
262
|
-
if (formContext && name) {
|
|
263
|
-
return (
|
|
264
|
-
<Controller
|
|
265
|
-
name={name}
|
|
266
|
-
control={formContext.control}
|
|
267
|
-
rules={{
|
|
268
|
-
required: required
|
|
269
|
-
? `${typeof label === 'string' ? label : 'File'} is required`
|
|
270
|
-
: false,
|
|
271
|
-
}}
|
|
272
|
-
render={({ field }) => (
|
|
273
|
-
<Form.Group className={containerClassName}>
|
|
274
|
-
<input
|
|
275
|
-
type="file"
|
|
276
|
-
ref={(e) => {
|
|
277
|
-
fileInputRef.current = e;
|
|
278
|
-
field.ref(e);
|
|
279
|
-
}}
|
|
280
|
-
style={{ display: 'none' }}
|
|
281
|
-
accept={accept}
|
|
282
|
-
multiple={multiple}
|
|
283
|
-
onChange={(e) => handleFileChange(e, field.onChange)}
|
|
284
|
-
onBlur={field.onBlur}
|
|
285
|
-
disabled={disabled || isUploading}
|
|
286
|
-
aria-invalid={!!displayError}
|
|
287
|
-
aria-describedby={displayError ? `${name}-error` : undefined}
|
|
288
|
-
/>
|
|
289
|
-
{label && (
|
|
290
|
-
<Form.Label className={labelClassName}>{label}</Form.Label>
|
|
291
|
-
)}
|
|
292
|
-
<div>
|
|
293
|
-
<Button
|
|
294
|
-
variant="secondary"
|
|
295
|
-
className={buttonClassName}
|
|
296
|
-
onClick={handleFileUploadClick}
|
|
297
|
-
disabled={disabled || isUploading}
|
|
298
|
-
>
|
|
299
|
-
{buttonText}
|
|
300
|
-
</Button>{' '}
|
|
301
|
-
{renderFileInfo()}
|
|
302
|
-
</div>
|
|
303
|
-
{displayError && (
|
|
304
|
-
<Form.Control.Feedback
|
|
305
|
-
type="invalid"
|
|
306
|
-
className={errorClassName}
|
|
307
|
-
style={{ display: 'block' }}
|
|
308
|
-
>
|
|
309
|
-
{displayError}
|
|
310
|
-
</Form.Control.Feedback>
|
|
311
|
-
)}
|
|
312
|
-
</Form.Group>
|
|
313
|
-
)}
|
|
314
|
-
/>
|
|
315
|
-
);
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Standalone mode (no form context)
|
|
319
|
-
return (
|
|
320
|
-
<Form.Group className={containerClassName}>
|
|
321
|
-
<input
|
|
322
|
-
type="file"
|
|
323
|
-
ref={fileInputRef}
|
|
324
|
-
style={{ display: 'none' }}
|
|
325
|
-
accept={accept}
|
|
326
|
-
multiple={multiple}
|
|
327
|
-
onChange={(e) => handleFileChange(e)}
|
|
328
|
-
disabled={disabled || isUploading}
|
|
329
|
-
/>
|
|
330
|
-
{label && <Form.Label className={labelClassName}>{label}</Form.Label>}
|
|
331
|
-
<div>
|
|
332
|
-
<Button
|
|
333
|
-
variant="secondary"
|
|
334
|
-
className={buttonClassName}
|
|
335
|
-
onClick={handleFileUploadClick}
|
|
336
|
-
disabled={disabled || isUploading}
|
|
337
|
-
>
|
|
338
|
-
{buttonText}
|
|
339
|
-
</Button>{' '}
|
|
340
|
-
{renderFileInfo()}
|
|
341
|
-
</div>
|
|
342
|
-
{displayError && (
|
|
343
|
-
<Form.Control.Feedback
|
|
344
|
-
type="invalid"
|
|
345
|
-
className={errorClassName}
|
|
346
|
-
style={{ display: 'block' }}
|
|
347
|
-
>
|
|
348
|
-
{displayError}
|
|
349
|
-
</Form.Control.Feedback>
|
|
350
|
-
)}
|
|
351
|
-
</Form.Group>
|
|
352
|
-
);
|
|
353
|
-
};
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
import React, { type FC, type KeyboardEvent, useRef, useState } from 'react';
|
|
2
|
-
import { Form } from 'react-bootstrap';
|
|
3
|
-
import ReactQuill from 'react-quill-new';
|
|
4
|
-
import 'react-quill-new/dist/quill.snow.css';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Props for the RichTextInput component
|
|
8
|
-
*/
|
|
9
|
-
export type RichTextInputProps = {
|
|
10
|
-
/** Whether the editor is in read-only mode */
|
|
11
|
-
readonly?: boolean;
|
|
12
|
-
/** Maximum number of characters allowed in the editor */
|
|
13
|
-
maxLength?: number;
|
|
14
|
-
/** Current value of the editor (HTML string) */
|
|
15
|
-
value?: string;
|
|
16
|
-
/** Callback function called when the content changes */
|
|
17
|
-
onChange?: (value: string) => void;
|
|
18
|
-
/** Whether the input should be styled as invalid */
|
|
19
|
-
isInvalid?: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* RichTextInput - A rich text editor component using ReactQuill
|
|
24
|
-
*
|
|
25
|
-
* Provides a WYSIWYG editor with support for headers, bold, italic, underline, and lists.
|
|
26
|
-
* Includes optional character counting and length validation.
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```tsx
|
|
30
|
-
* <RichTextInput
|
|
31
|
-
* value={content}
|
|
32
|
-
* onChange={setContent}
|
|
33
|
-
* maxLength={1000}
|
|
34
|
-
* />
|
|
35
|
-
* ```
|
|
36
|
-
*/
|
|
37
|
-
export const RichTextInput: FC<RichTextInputProps> = ({
|
|
38
|
-
readonly = false,
|
|
39
|
-
maxLength,
|
|
40
|
-
value = '',
|
|
41
|
-
onChange,
|
|
42
|
-
isInvalid = false,
|
|
43
|
-
}) => {
|
|
44
|
-
const reactQuillRef = useRef<ReactQuill>(null);
|
|
45
|
-
const [count, setCount] = useState(value.length);
|
|
46
|
-
|
|
47
|
-
const checkCharacterCount = (event: KeyboardEvent) => {
|
|
48
|
-
const currentRef = reactQuillRef.current;
|
|
49
|
-
if (currentRef != null && maxLength != null) {
|
|
50
|
-
const unprivilegedEditor = (
|
|
51
|
-
currentRef as ReactQuill & {
|
|
52
|
-
unprivilegedEditor?: { getHTML: () => string };
|
|
53
|
-
}
|
|
54
|
-
).unprivilegedEditor;
|
|
55
|
-
if (unprivilegedEditor) {
|
|
56
|
-
const length = unprivilegedEditor.getHTML().length;
|
|
57
|
-
if (length >= maxLength && event.key !== 'Backspace') {
|
|
58
|
-
event.preventDefault();
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
const setContentLength = () => {
|
|
65
|
-
const currentRef = reactQuillRef.current;
|
|
66
|
-
if (currentRef != null && maxLength != null) {
|
|
67
|
-
const unprivilegedEditor = (
|
|
68
|
-
currentRef as ReactQuill & {
|
|
69
|
-
unprivilegedEditor?: { getHTML: () => string };
|
|
70
|
-
}
|
|
71
|
-
).unprivilegedEditor;
|
|
72
|
-
if (unprivilegedEditor) {
|
|
73
|
-
const length = unprivilegedEditor.getHTML().length;
|
|
74
|
-
setCount(length);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const modules = {
|
|
80
|
-
toolbar: readonly
|
|
81
|
-
? false
|
|
82
|
-
: [
|
|
83
|
-
[{ header: [1, 2, 3, 4, 5, 6, false] }],
|
|
84
|
-
['bold', 'italic', 'underline'],
|
|
85
|
-
[{ list: 'ordered' }, { list: 'bullet' }],
|
|
86
|
-
],
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
const formats = ['header', 'bold', 'italic', 'underline', 'list', 'bullet'];
|
|
90
|
-
|
|
91
|
-
return (
|
|
92
|
-
<>
|
|
93
|
-
<ReactQuill
|
|
94
|
-
ref={reactQuillRef}
|
|
95
|
-
theme="snow"
|
|
96
|
-
onKeyDown={checkCharacterCount}
|
|
97
|
-
onKeyUp={setContentLength}
|
|
98
|
-
formats={formats}
|
|
99
|
-
modules={modules}
|
|
100
|
-
value={value}
|
|
101
|
-
onChange={onChange}
|
|
102
|
-
readOnly={readonly}
|
|
103
|
-
className={isInvalid ? 'is-invalid' : ''}
|
|
104
|
-
/>
|
|
105
|
-
{maxLength && (
|
|
106
|
-
<Form.Text className={count > maxLength ? 'text-danger' : 'text-muted'}>
|
|
107
|
-
{count}/{maxLength} characters
|
|
108
|
-
</Form.Text>
|
|
109
|
-
)}
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
112
|
-
};
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import React, { type ChangeEvent, type FC } from 'react';
|
|
2
|
-
import { Form } from 'react-bootstrap';
|
|
3
|
-
import { Controller, useFormContext } from 'react-hook-form';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Props for the SelectInput component
|
|
7
|
-
*/
|
|
8
|
-
export type SelectInputProps = {
|
|
9
|
-
/** The name of the select input field */
|
|
10
|
-
name: string;
|
|
11
|
-
/** Label text displayed for the select */
|
|
12
|
-
label?: string;
|
|
13
|
-
/** Array of option strings to display in the dropdown */
|
|
14
|
-
options: string[];
|
|
15
|
-
/** Placeholder text shown when no option is selected (default: "Select...") */
|
|
16
|
-
helpText?: string;
|
|
17
|
-
/** Size of the select control */
|
|
18
|
-
controlSize?: 'sm' | 'lg';
|
|
19
|
-
/** Current selected value (standalone mode) */
|
|
20
|
-
value?: string;
|
|
21
|
-
/** Callback function called when the selection changes */
|
|
22
|
-
onChange?: (value: string) => void;
|
|
23
|
-
/** Whether the select is disabled */
|
|
24
|
-
disabled?: boolean;
|
|
25
|
-
/** Whether the select is required */
|
|
26
|
-
required?: boolean;
|
|
27
|
-
/** Array of option values to display in bold */
|
|
28
|
-
highlightValues?: string[];
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* SelectInput - A dropdown select component with react-hook-form integration
|
|
33
|
-
*
|
|
34
|
-
* Provides a select dropdown that works both standalone and with react-hook-form.
|
|
35
|
-
* Supports option highlighting, custom placeholder text, and automatic validation.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* ```tsx
|
|
39
|
-
* // With react-hook-form
|
|
40
|
-
* <SelectInput
|
|
41
|
-
* name="country"
|
|
42
|
-
* label="Country"
|
|
43
|
-
* options={["USA", "Canada", "UK"]}
|
|
44
|
-
* required
|
|
45
|
-
* />
|
|
46
|
-
*
|
|
47
|
-
* // Standalone mode
|
|
48
|
-
* <SelectInput
|
|
49
|
-
* name="role"
|
|
50
|
-
* options={["Admin", "User", "Guest"]}
|
|
51
|
-
* value={selectedRole}
|
|
52
|
-
* onChange={setSelectedRole}
|
|
53
|
-
* highlightValues={["Admin"]}
|
|
54
|
-
* />
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export const SelectInput: FC<SelectInputProps> = ({
|
|
58
|
-
name,
|
|
59
|
-
label,
|
|
60
|
-
options = [],
|
|
61
|
-
helpText = 'Select...',
|
|
62
|
-
controlSize,
|
|
63
|
-
value,
|
|
64
|
-
onChange,
|
|
65
|
-
disabled = false,
|
|
66
|
-
required = false,
|
|
67
|
-
highlightValues = [],
|
|
68
|
-
}) => {
|
|
69
|
-
const formContext = useFormContext();
|
|
70
|
-
|
|
71
|
-
const getFieldError = (fieldName: string) => {
|
|
72
|
-
try {
|
|
73
|
-
const error = formContext?.formState?.errors?.[fieldName];
|
|
74
|
-
return error?.message as string | undefined;
|
|
75
|
-
} catch {
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const errorMessage = getFieldError(name);
|
|
81
|
-
const isInvalid = !!errorMessage;
|
|
82
|
-
|
|
83
|
-
const handleSelectChange = (event: ChangeEvent<HTMLSelectElement>) => {
|
|
84
|
-
onChange?.(event.currentTarget.value);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
const renderOptions = () => (
|
|
88
|
-
<>
|
|
89
|
-
<option value="">{helpText.trim()}</option>
|
|
90
|
-
{options.map((option: string) => {
|
|
91
|
-
const style = highlightValues.includes(option)
|
|
92
|
-
? { fontWeight: 'bold' as const }
|
|
93
|
-
: {};
|
|
94
|
-
const key = option.replace(/ /g, '');
|
|
95
|
-
return (
|
|
96
|
-
<option style={style} key={key} value={key}>
|
|
97
|
-
{option.trim()}
|
|
98
|
-
</option>
|
|
99
|
-
);
|
|
100
|
-
})}
|
|
101
|
-
</>
|
|
102
|
-
);
|
|
103
|
-
|
|
104
|
-
// Integrated with react-hook-form
|
|
105
|
-
if (formContext) {
|
|
106
|
-
return (
|
|
107
|
-
<Controller
|
|
108
|
-
name={name}
|
|
109
|
-
control={formContext.control}
|
|
110
|
-
rules={{
|
|
111
|
-
required: required ? `${label || 'This field'} is required` : false,
|
|
112
|
-
}}
|
|
113
|
-
render={({ field }) => (
|
|
114
|
-
<Form.Select
|
|
115
|
-
{...field}
|
|
116
|
-
onChange={(e) => {
|
|
117
|
-
field.onChange(e);
|
|
118
|
-
onChange?.(e.target.value);
|
|
119
|
-
}}
|
|
120
|
-
required={required}
|
|
121
|
-
size={controlSize}
|
|
122
|
-
disabled={disabled}
|
|
123
|
-
isInvalid={isInvalid}
|
|
124
|
-
>
|
|
125
|
-
{renderOptions()}
|
|
126
|
-
</Form.Select>
|
|
127
|
-
)}
|
|
128
|
-
/>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Standalone mode
|
|
133
|
-
return (
|
|
134
|
-
<Form.Select
|
|
135
|
-
required={required}
|
|
136
|
-
size={controlSize}
|
|
137
|
-
value={value || ''}
|
|
138
|
-
onChange={handleSelectChange}
|
|
139
|
-
disabled={disabled}
|
|
140
|
-
>
|
|
141
|
-
{renderOptions()}
|
|
142
|
-
</Form.Select>
|
|
143
|
-
);
|
|
144
|
-
};
|