@react-magma/dropzone 13.0.1-rc.0 → 14.0.0-next.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/dist/fileuploader.js +1 -1
- package/dist/fileuploader.js.map +1 -1
- package/dist/fileuploader.modern.js +83 -0
- package/dist/fileuploader.modern.js.map +1 -0
- package/dist/fileuploader.modern.module.js +1 -1
- package/dist/fileuploader.modern.module.js.map +1 -1
- package/dist/fileuploader.umd.js +1 -1
- package/dist/fileuploader.umd.js.map +1 -1
- package/dist/src/src/components/dropzone/Dropzone.d.ts +101 -101
- package/dist/src/src/components/dropzone/Dropzone.stories.d.ts +48 -0
- package/dist/src/src/components/dropzone/Dropzone.test.d.ts +1 -1
- package/dist/src/src/components/dropzone/FileIcon.d.ts +8 -7
- package/dist/src/src/components/dropzone/FilePreview.d.ts +15 -15
- package/dist/src/src/components/dropzone/Preview.d.ts +18 -18
- package/dist/src/src/components/dropzone/index.d.ts +4 -4
- package/dist/src/src/components/dropzone/utils.d.ts +1 -1
- package/dist/src/src/index.d.ts +1 -1
- package/package.json +22 -20
- package/src/components/dropzone/Dropzone.stories.tsx +0 -4
- package/src/components/dropzone/Dropzone.test.js +125 -199
- package/src/components/dropzone/Dropzone.tsx +175 -81
- package/src/components/dropzone/Preview.tsx +150 -152
- package/src/components/dropzone/utils.ts +0 -1
- package/dist/fileuploader.modern.mjs +0 -80
- package/dist/fileuploader.modern.mjs.map +0 -1
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
useGenerateId,
|
|
32
32
|
useIsInverse,
|
|
33
33
|
styled,
|
|
34
|
+
Announce,
|
|
35
|
+
VisuallyHidden,
|
|
34
36
|
} from 'react-magma-dom';
|
|
35
37
|
import { CloudUploadIcon } from 'react-magma-icons';
|
|
36
38
|
|
|
@@ -188,13 +190,20 @@ const Wrapper = styled.div<{ isInverse?: boolean }>`
|
|
|
188
190
|
padding: ${({ theme }) => theme.spaceScale.spacing01};
|
|
189
191
|
`;
|
|
190
192
|
|
|
193
|
+
const PreviewList = styled.ul`
|
|
194
|
+
list-style: none;
|
|
195
|
+
padding: 0;
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const PreviewItem = styled.li``;
|
|
199
|
+
|
|
191
200
|
export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
192
201
|
(props, ref) => {
|
|
193
202
|
const {
|
|
194
203
|
accept,
|
|
195
204
|
containerStyle,
|
|
196
205
|
disabled,
|
|
197
|
-
|
|
206
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
198
207
|
dropzoneOptions = {
|
|
199
208
|
multiple: true,
|
|
200
209
|
},
|
|
@@ -222,16 +231,19 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
222
231
|
|
|
223
232
|
const [files, setFiles] = React.useState<FilePreview[]>([]);
|
|
224
233
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
|
234
|
+
const [announcement, setAnnouncement] = React.useState<string>('');
|
|
225
235
|
|
|
226
236
|
const isInverse = useIsInverse(isInverseProp);
|
|
227
237
|
const theme: ThemeInterface = React.useContext(ThemeContext);
|
|
228
238
|
const i18n: I18nInterface = React.useContext(I18nContext);
|
|
229
239
|
const id = useGenerateId(defaultId);
|
|
240
|
+
const helperMessageId = useGenerateId(`${id}-helper`);
|
|
241
|
+
|
|
242
|
+
const browseFileButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
230
243
|
|
|
231
244
|
const onDrop = React.useCallback(
|
|
232
245
|
(acceptedFiles: FilePreview[], rejectedFiles: FileRejection[]) => {
|
|
233
|
-
|
|
234
|
-
...files,
|
|
246
|
+
const newFiles = [
|
|
235
247
|
...acceptedFiles.map((file: FilePreview) =>
|
|
236
248
|
Object.assign(file, {
|
|
237
249
|
preview: URL.createObjectURL(file),
|
|
@@ -243,9 +255,25 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
243
255
|
errors,
|
|
244
256
|
})
|
|
245
257
|
),
|
|
246
|
-
]
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
setFiles((prevFiles: FilePreview[]) => [...prevFiles, ...newFiles]);
|
|
261
|
+
|
|
262
|
+
if (acceptedFiles.length > 0) {
|
|
263
|
+
const fileNames = acceptedFiles.map(file => file.name).join(', ');
|
|
264
|
+
const message =
|
|
265
|
+
acceptedFiles.length === 1
|
|
266
|
+
? i18n.dropzone.fileAdded.replace(/\{fileName\}/g, fileNames)
|
|
267
|
+
: i18n.dropzone.filesAdded
|
|
268
|
+
.replace(/\{count\}/g, acceptedFiles.length.toString())
|
|
269
|
+
.replace(/\{fileNames\}/g, fileNames);
|
|
270
|
+
|
|
271
|
+
setAnnouncement(message);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
browseFileButtonRef.current && browseFileButtonRef.current.focus();
|
|
247
275
|
},
|
|
248
|
-
[]
|
|
276
|
+
[i18n]
|
|
249
277
|
);
|
|
250
278
|
|
|
251
279
|
const {
|
|
@@ -275,50 +303,84 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
275
303
|
|
|
276
304
|
const inputProps = getInputProps({ id });
|
|
277
305
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
306
|
+
let dragState: DragState = 'default';
|
|
307
|
+
|
|
308
|
+
if (errorMessage) {
|
|
309
|
+
dragState = 'error';
|
|
310
|
+
} else if (isDragAccept) {
|
|
311
|
+
dragState = 'dragAccept';
|
|
312
|
+
} else if (isDragReject) {
|
|
313
|
+
dragState = 'dragReject';
|
|
314
|
+
} else if (isDragActive) {
|
|
315
|
+
dragState = 'dragActive';
|
|
316
|
+
}
|
|
287
317
|
|
|
288
318
|
const handleRemoveFile = (removedFile: FilePreview) => {
|
|
289
|
-
setFiles(
|
|
290
|
-
|
|
291
|
-
|
|
319
|
+
setFiles(prevFiles => prevFiles.filter(file => file !== removedFile));
|
|
320
|
+
|
|
321
|
+
if (onRemoveFile && typeof onRemoveFile === 'function') {
|
|
292
322
|
onRemoveFile(removedFile);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const message = i18n.dropzone.fileRemoved.replace(
|
|
326
|
+
/\{fileName\}/g,
|
|
327
|
+
removedFile.name
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
setAnnouncement(message);
|
|
331
|
+
|
|
332
|
+
browseFileButtonRef.current && browseFileButtonRef.current.focus();
|
|
293
333
|
};
|
|
294
334
|
|
|
295
335
|
const handleDeleteFile = (removedFile: FilePreview) => {
|
|
296
|
-
setFiles(
|
|
297
|
-
|
|
298
|
-
|
|
336
|
+
setFiles(prevFiles => prevFiles.filter(file => file !== removedFile));
|
|
337
|
+
|
|
338
|
+
if (onDeleteFile && typeof onDeleteFile === 'function') {
|
|
299
339
|
onDeleteFile(removedFile);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const message = i18n.dropzone.fileDeleted.replace(
|
|
343
|
+
/\{fileName\}/g,
|
|
344
|
+
removedFile.name
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
setAnnouncement(message);
|
|
348
|
+
|
|
349
|
+
browseFileButtonRef.current && browseFileButtonRef.current.focus();
|
|
300
350
|
};
|
|
301
351
|
|
|
302
|
-
const setProgress = (
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
352
|
+
const setProgress = (progressProps: {
|
|
353
|
+
percent: number;
|
|
354
|
+
file: FilePreview;
|
|
355
|
+
}) => {
|
|
356
|
+
setFiles(prevFiles =>
|
|
357
|
+
prevFiles.map(file =>
|
|
358
|
+
file === progressProps.file
|
|
306
359
|
? Object.assign(file, {
|
|
307
360
|
processor: {
|
|
308
361
|
...file.processor,
|
|
309
|
-
percent: `${
|
|
362
|
+
percent: `${progressProps.percent}%`,
|
|
310
363
|
status: 'pending',
|
|
311
364
|
},
|
|
312
365
|
})
|
|
313
366
|
: file
|
|
314
367
|
)
|
|
315
368
|
);
|
|
369
|
+
|
|
370
|
+
// Announce progress every 25% to avoid too many announcements
|
|
371
|
+
if (progressProps.percent > 0 && progressProps.percent % 25 === 0) {
|
|
372
|
+
const message = i18n.dropzone.fileUploading
|
|
373
|
+
.replace(/\{fileName\}/g, progressProps.file.name)
|
|
374
|
+
.replace(/\{percent\}/g, progressProps.percent.toString());
|
|
375
|
+
|
|
376
|
+
setAnnouncement(message);
|
|
377
|
+
}
|
|
316
378
|
};
|
|
317
379
|
|
|
318
|
-
const setFinished = (
|
|
319
|
-
setFiles(
|
|
320
|
-
|
|
321
|
-
file ===
|
|
380
|
+
const setFinished = (finishedProps: { file: FilePreview }) => {
|
|
381
|
+
setFiles(prevFiles =>
|
|
382
|
+
prevFiles.map(file =>
|
|
383
|
+
file === finishedProps.file
|
|
322
384
|
? Object.assign(file, {
|
|
323
385
|
processor: {
|
|
324
386
|
...file.processor,
|
|
@@ -329,14 +391,25 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
329
391
|
: file
|
|
330
392
|
)
|
|
331
393
|
);
|
|
394
|
+
|
|
395
|
+
// Announce successful upload
|
|
396
|
+
const message = i18n.dropzone.fileUploaded.replace(
|
|
397
|
+
/\{fileName\}/g,
|
|
398
|
+
finishedProps.file.name
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
setAnnouncement(message);
|
|
332
402
|
};
|
|
333
403
|
|
|
334
|
-
const setError = (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
404
|
+
const setError = (errorProps: {
|
|
405
|
+
errors: FileError[];
|
|
406
|
+
file: FilePreview;
|
|
407
|
+
}) => {
|
|
408
|
+
setFiles(prevFiles =>
|
|
409
|
+
prevFiles.map(file =>
|
|
410
|
+
file === errorProps.file
|
|
338
411
|
? Object.assign(file, {
|
|
339
|
-
errors:
|
|
412
|
+
errors: errorProps.errors,
|
|
340
413
|
processor: { ...file.processor, status: 'error' },
|
|
341
414
|
})
|
|
342
415
|
: file
|
|
@@ -344,23 +417,6 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
344
417
|
);
|
|
345
418
|
};
|
|
346
419
|
|
|
347
|
-
const formatError = (
|
|
348
|
-
code: string | null,
|
|
349
|
-
constraints: { maxFiles?: number; minFiles?: number }
|
|
350
|
-
) => {
|
|
351
|
-
if (code === null) return null;
|
|
352
|
-
const error = i18n.dropzone.errors[code];
|
|
353
|
-
|
|
354
|
-
switch (code) {
|
|
355
|
-
case 'too-many-files':
|
|
356
|
-
return `${error.message} ${constraints.maxFiles} ${i18n.dropzone.files}.`;
|
|
357
|
-
case 'too-few-files':
|
|
358
|
-
return `${error.message} ${constraints.minFiles} ${i18n.dropzone.files}.`;
|
|
359
|
-
default:
|
|
360
|
-
return error.message;
|
|
361
|
-
}
|
|
362
|
-
};
|
|
363
|
-
|
|
364
420
|
React.useEffect(
|
|
365
421
|
() => () => {
|
|
366
422
|
files.forEach(
|
|
@@ -371,38 +427,61 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
371
427
|
);
|
|
372
428
|
|
|
373
429
|
React.useEffect(() => {
|
|
430
|
+
const formatError = (
|
|
431
|
+
code: string | null,
|
|
432
|
+
constraints: { maxFiles?: number; minFiles?: number }
|
|
433
|
+
) => {
|
|
434
|
+
if (code === null) return null;
|
|
435
|
+
const error = i18n.dropzone.errors[code];
|
|
436
|
+
|
|
437
|
+
switch (code) {
|
|
438
|
+
case 'too-many-files':
|
|
439
|
+
return `${error.message} ${constraints.maxFiles} ${i18n.dropzone.files}.`;
|
|
440
|
+
case 'too-few-files':
|
|
441
|
+
return `${error.message} ${constraints.minFiles} ${i18n.dropzone.files}.`;
|
|
442
|
+
default:
|
|
443
|
+
return error.message;
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
374
447
|
const minFileError = minFiles && files.length < minFiles;
|
|
375
448
|
const maxFileError = maxFiles && files.length > maxFiles;
|
|
376
449
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
);
|
|
450
|
+
let errorCode: string | null = null;
|
|
451
|
+
|
|
452
|
+
if (maxFileError) {
|
|
453
|
+
errorCode = 'too-many-files';
|
|
454
|
+
} else if (minFileError) {
|
|
455
|
+
errorCode = 'too-few-files';
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
setErrorMessage(formatError(errorCode, { minFiles, maxFiles }));
|
|
387
459
|
|
|
388
460
|
if (sendFiles && files.length > 0 && !maxFileError && !minFileError) {
|
|
389
|
-
setFiles((
|
|
390
|
-
return
|
|
391
|
-
!file.errors &&
|
|
392
|
-
!file.processor &&
|
|
393
|
-
onSendFile &&
|
|
461
|
+
setFiles((prevFiles: FilePreview[]) => {
|
|
462
|
+
return prevFiles.map((file: FilePreview) => {
|
|
463
|
+
if (!file.errors && !file.processor && onSendFile) {
|
|
394
464
|
onSendFile({
|
|
395
465
|
file,
|
|
396
466
|
onError: setError,
|
|
397
467
|
onFinish: setFinished,
|
|
398
468
|
onProgress: setProgress,
|
|
399
469
|
});
|
|
470
|
+
}
|
|
400
471
|
|
|
401
472
|
return file;
|
|
402
473
|
});
|
|
403
474
|
});
|
|
404
475
|
}
|
|
405
|
-
}, [
|
|
476
|
+
}, [
|
|
477
|
+
sendFiles,
|
|
478
|
+
files.length,
|
|
479
|
+
onSendFile,
|
|
480
|
+
maxFiles,
|
|
481
|
+
minFiles,
|
|
482
|
+
i18n.dropzone.errors,
|
|
483
|
+
i18n.dropzone.files,
|
|
484
|
+
]);
|
|
406
485
|
|
|
407
486
|
return (
|
|
408
487
|
<InverseContext.Provider value={{ isInverse }}>
|
|
@@ -419,7 +498,11 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
419
498
|
messageStyle={{ minHeight: 0 }}
|
|
420
499
|
data-testid={testId}
|
|
421
500
|
>
|
|
422
|
-
<HelperMessage
|
|
501
|
+
<HelperMessage
|
|
502
|
+
id={helperMessageId}
|
|
503
|
+
theme={theme}
|
|
504
|
+
isInverse={isInverse}
|
|
505
|
+
>
|
|
423
506
|
{helperMessage}
|
|
424
507
|
</HelperMessage>
|
|
425
508
|
<Container
|
|
@@ -454,6 +537,8 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
454
537
|
isInverse={isInverse}
|
|
455
538
|
onClick={open}
|
|
456
539
|
style={{ margin: 0 }}
|
|
540
|
+
ref={browseFileButtonRef}
|
|
541
|
+
aria-describedby={helperMessage ? helperMessageId : undefined}
|
|
457
542
|
>
|
|
458
543
|
{i18n.dropzone.browseFiles}
|
|
459
544
|
</Button>
|
|
@@ -479,6 +564,8 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
479
564
|
onClick={open}
|
|
480
565
|
style={{ margin: 0 }}
|
|
481
566
|
variant={ButtonVariant.solid}
|
|
567
|
+
ref={browseFileButtonRef}
|
|
568
|
+
aria-describedby={helperMessage ? helperMessageId : undefined}
|
|
482
569
|
>
|
|
483
570
|
{i18n.dropzone.browseFiles}
|
|
484
571
|
</Button>
|
|
@@ -486,19 +573,26 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
486
573
|
)}
|
|
487
574
|
</Container>
|
|
488
575
|
</FormFieldContainer>
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
576
|
+
<PreviewList>
|
|
577
|
+
{files.map((file: FilePreview) => (
|
|
578
|
+
<PreviewItem key={file.name}>
|
|
579
|
+
<Preview
|
|
580
|
+
accept={accept}
|
|
581
|
+
file={file}
|
|
582
|
+
isInverse={isInverse}
|
|
583
|
+
maxSize={maxSize}
|
|
584
|
+
minSize={minSize}
|
|
585
|
+
onDeleteFile={handleDeleteFile}
|
|
586
|
+
onRemoveFile={handleRemoveFile}
|
|
587
|
+
thumbnails={thumbnails}
|
|
588
|
+
/>
|
|
589
|
+
</PreviewItem>
|
|
590
|
+
))}
|
|
591
|
+
</PreviewList>
|
|
592
|
+
|
|
593
|
+
<VisuallyHidden>
|
|
594
|
+
<Announce>{announcement}</Announce>
|
|
595
|
+
</VisuallyHidden>
|
|
502
596
|
</InverseContext.Provider>
|
|
503
597
|
);
|
|
504
598
|
}
|