@react-magma/dropzone 14.0.0-next.1 → 14.0.0-next.3
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 +15 -12
- package/dist/fileuploader.modern.js.map +1 -1
- 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/package.json +1 -1
- package/src/components/dropzone/Dropzone.test.js +23 -12
- package/src/components/dropzone/Dropzone.tsx +176 -79
- package/src/components/dropzone/Preview.tsx +2 -2
|
@@ -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
|
|
|
@@ -187,6 +189,14 @@ const Wrapper = styled.div<{ isInverse?: boolean }>`
|
|
|
187
189
|
font-weight: 500;
|
|
188
190
|
padding: ${({ theme }) => theme.spaceScale.spacing01};
|
|
189
191
|
`;
|
|
192
|
+
|
|
193
|
+
const PreviewList = styled.ul`
|
|
194
|
+
list-style: none;
|
|
195
|
+
padding: 0;
|
|
196
|
+
`;
|
|
197
|
+
|
|
198
|
+
const PreviewItem = styled.li``;
|
|
199
|
+
|
|
190
200
|
export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
191
201
|
(props, ref) => {
|
|
192
202
|
const {
|
|
@@ -221,16 +231,19 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
221
231
|
|
|
222
232
|
const [files, setFiles] = React.useState<FilePreview[]>([]);
|
|
223
233
|
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
|
234
|
+
const [announcement, setAnnouncement] = React.useState<string>('');
|
|
224
235
|
|
|
225
236
|
const isInverse = useIsInverse(isInverseProp);
|
|
226
237
|
const theme: ThemeInterface = React.useContext(ThemeContext);
|
|
227
238
|
const i18n: I18nInterface = React.useContext(I18nContext);
|
|
228
239
|
const id = useGenerateId(defaultId);
|
|
240
|
+
const helperMessageId = useGenerateId(`${id}-helper`);
|
|
241
|
+
|
|
242
|
+
const browseFileButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
229
243
|
|
|
230
244
|
const onDrop = React.useCallback(
|
|
231
245
|
(acceptedFiles: FilePreview[], rejectedFiles: FileRejection[]) => {
|
|
232
|
-
|
|
233
|
-
...files,
|
|
246
|
+
const newFiles = [
|
|
234
247
|
...acceptedFiles.map((file: FilePreview) =>
|
|
235
248
|
Object.assign(file, {
|
|
236
249
|
preview: URL.createObjectURL(file),
|
|
@@ -242,9 +255,25 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
242
255
|
errors,
|
|
243
256
|
})
|
|
244
257
|
),
|
|
245
|
-
]
|
|
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();
|
|
246
275
|
},
|
|
247
|
-
[]
|
|
276
|
+
[i18n]
|
|
248
277
|
);
|
|
249
278
|
|
|
250
279
|
const {
|
|
@@ -274,50 +303,84 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
274
303
|
|
|
275
304
|
const inputProps = getInputProps({ id });
|
|
276
305
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
}
|
|
286
317
|
|
|
287
318
|
const handleRemoveFile = (removedFile: FilePreview) => {
|
|
288
|
-
setFiles(
|
|
289
|
-
|
|
290
|
-
|
|
319
|
+
setFiles(prevFiles => prevFiles.filter(file => file !== removedFile));
|
|
320
|
+
|
|
321
|
+
if (onRemoveFile && typeof onRemoveFile === 'function') {
|
|
291
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();
|
|
292
333
|
};
|
|
293
334
|
|
|
294
335
|
const handleDeleteFile = (removedFile: FilePreview) => {
|
|
295
|
-
setFiles(
|
|
296
|
-
|
|
297
|
-
|
|
336
|
+
setFiles(prevFiles => prevFiles.filter(file => file !== removedFile));
|
|
337
|
+
|
|
338
|
+
if (onDeleteFile && typeof onDeleteFile === 'function') {
|
|
298
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();
|
|
299
350
|
};
|
|
300
351
|
|
|
301
|
-
const setProgress = (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
352
|
+
const setProgress = (progressProps: {
|
|
353
|
+
percent: number;
|
|
354
|
+
file: FilePreview;
|
|
355
|
+
}) => {
|
|
356
|
+
setFiles(prevFiles =>
|
|
357
|
+
prevFiles.map(file =>
|
|
358
|
+
file === progressProps.file
|
|
305
359
|
? Object.assign(file, {
|
|
306
360
|
processor: {
|
|
307
361
|
...file.processor,
|
|
308
|
-
percent: `${
|
|
362
|
+
percent: `${progressProps.percent}%`,
|
|
309
363
|
status: 'pending',
|
|
310
364
|
},
|
|
311
365
|
})
|
|
312
366
|
: file
|
|
313
367
|
)
|
|
314
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
|
+
}
|
|
315
378
|
};
|
|
316
379
|
|
|
317
|
-
const setFinished = (
|
|
318
|
-
setFiles(
|
|
319
|
-
|
|
320
|
-
file ===
|
|
380
|
+
const setFinished = (finishedProps: { file: FilePreview }) => {
|
|
381
|
+
setFiles(prevFiles =>
|
|
382
|
+
prevFiles.map(file =>
|
|
383
|
+
file === finishedProps.file
|
|
321
384
|
? Object.assign(file, {
|
|
322
385
|
processor: {
|
|
323
386
|
...file.processor,
|
|
@@ -328,14 +391,25 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
328
391
|
: file
|
|
329
392
|
)
|
|
330
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);
|
|
331
402
|
};
|
|
332
403
|
|
|
333
|
-
const setError = (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
const setError = (errorProps: {
|
|
405
|
+
errors: FileError[];
|
|
406
|
+
file: FilePreview;
|
|
407
|
+
}) => {
|
|
408
|
+
setFiles(prevFiles =>
|
|
409
|
+
prevFiles.map(file =>
|
|
410
|
+
file === errorProps.file
|
|
337
411
|
? Object.assign(file, {
|
|
338
|
-
errors:
|
|
412
|
+
errors: errorProps.errors,
|
|
339
413
|
processor: { ...file.processor, status: 'error' },
|
|
340
414
|
})
|
|
341
415
|
: file
|
|
@@ -343,22 +417,6 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
343
417
|
);
|
|
344
418
|
};
|
|
345
419
|
|
|
346
|
-
const formatError = (
|
|
347
|
-
code: string | null,
|
|
348
|
-
constraints: { maxFiles?: number; minFiles?: number }
|
|
349
|
-
) => {
|
|
350
|
-
if (code === null) return null;
|
|
351
|
-
const error = i18n.dropzone.errors[code];
|
|
352
|
-
switch (code) {
|
|
353
|
-
case 'too-many-files':
|
|
354
|
-
return `${error.message} ${constraints.maxFiles} ${i18n.dropzone.files}.`;
|
|
355
|
-
case 'too-few-files':
|
|
356
|
-
return `${error.message} ${constraints.minFiles} ${i18n.dropzone.files}.`;
|
|
357
|
-
default:
|
|
358
|
-
return error.message;
|
|
359
|
-
}
|
|
360
|
-
};
|
|
361
|
-
|
|
362
420
|
React.useEffect(
|
|
363
421
|
() => () => {
|
|
364
422
|
files.forEach(
|
|
@@ -369,37 +427,61 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
369
427
|
);
|
|
370
428
|
|
|
371
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
|
+
|
|
372
447
|
const minFileError = minFiles && files.length < minFiles;
|
|
373
448
|
const maxFileError = maxFiles && files.length > maxFiles;
|
|
374
449
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
);
|
|
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 }));
|
|
385
459
|
|
|
386
460
|
if (sendFiles && files.length > 0 && !maxFileError && !minFileError) {
|
|
387
|
-
setFiles((
|
|
388
|
-
return
|
|
389
|
-
!file.errors &&
|
|
390
|
-
!file.processor &&
|
|
391
|
-
onSendFile &&
|
|
461
|
+
setFiles((prevFiles: FilePreview[]) => {
|
|
462
|
+
return prevFiles.map((file: FilePreview) => {
|
|
463
|
+
if (!file.errors && !file.processor && onSendFile) {
|
|
392
464
|
onSendFile({
|
|
393
465
|
file,
|
|
394
466
|
onError: setError,
|
|
395
467
|
onFinish: setFinished,
|
|
396
468
|
onProgress: setProgress,
|
|
397
469
|
});
|
|
470
|
+
}
|
|
471
|
+
|
|
398
472
|
return file;
|
|
399
473
|
});
|
|
400
474
|
});
|
|
401
475
|
}
|
|
402
|
-
}, [
|
|
476
|
+
}, [
|
|
477
|
+
sendFiles,
|
|
478
|
+
files.length,
|
|
479
|
+
onSendFile,
|
|
480
|
+
maxFiles,
|
|
481
|
+
minFiles,
|
|
482
|
+
i18n.dropzone.errors,
|
|
483
|
+
i18n.dropzone.files,
|
|
484
|
+
]);
|
|
403
485
|
|
|
404
486
|
return (
|
|
405
487
|
<InverseContext.Provider value={{ isInverse }}>
|
|
@@ -416,7 +498,11 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
416
498
|
messageStyle={{ minHeight: 0 }}
|
|
417
499
|
data-testid={testId}
|
|
418
500
|
>
|
|
419
|
-
<HelperMessage
|
|
501
|
+
<HelperMessage
|
|
502
|
+
id={helperMessageId}
|
|
503
|
+
theme={theme}
|
|
504
|
+
isInverse={isInverse}
|
|
505
|
+
>
|
|
420
506
|
{helperMessage}
|
|
421
507
|
</HelperMessage>
|
|
422
508
|
<Container
|
|
@@ -451,6 +537,8 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
451
537
|
isInverse={isInverse}
|
|
452
538
|
onClick={open}
|
|
453
539
|
style={{ margin: 0 }}
|
|
540
|
+
ref={browseFileButtonRef}
|
|
541
|
+
aria-describedby={helperMessage ? helperMessageId : undefined}
|
|
454
542
|
>
|
|
455
543
|
{i18n.dropzone.browseFiles}
|
|
456
544
|
</Button>
|
|
@@ -476,6 +564,8 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
476
564
|
onClick={open}
|
|
477
565
|
style={{ margin: 0 }}
|
|
478
566
|
variant={ButtonVariant.solid}
|
|
567
|
+
ref={browseFileButtonRef}
|
|
568
|
+
aria-describedby={helperMessage ? helperMessageId : undefined}
|
|
479
569
|
>
|
|
480
570
|
{i18n.dropzone.browseFiles}
|
|
481
571
|
</Button>
|
|
@@ -483,19 +573,26 @@ export const Dropzone = React.forwardRef<HTMLInputElement, DropzoneProps>(
|
|
|
483
573
|
)}
|
|
484
574
|
</Container>
|
|
485
575
|
</FormFieldContainer>
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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>
|
|
499
596
|
</InverseContext.Provider>
|
|
500
597
|
);
|
|
501
598
|
}
|
|
@@ -218,7 +218,7 @@ export const Preview = forwardRef<HTMLDivElement, PreviewProps>(
|
|
|
218
218
|
onClick={handleRemoveFile}
|
|
219
219
|
variant={ButtonVariant.link}
|
|
220
220
|
color={ButtonColor.secondary}
|
|
221
|
-
aria-label={i18n.dropzone.removeFile}
|
|
221
|
+
aria-label={`${i18n.dropzone.removeFile} ${file.name}`}
|
|
222
222
|
icon={<CloseIcon />}
|
|
223
223
|
/>
|
|
224
224
|
</StatusIcons>
|
|
@@ -248,7 +248,7 @@ export const Preview = forwardRef<HTMLDivElement, PreviewProps>(
|
|
|
248
248
|
onClick={handleDeleteFile}
|
|
249
249
|
variant={ButtonVariant.link}
|
|
250
250
|
color={ButtonColor.secondary}
|
|
251
|
-
aria-label={i18n.dropzone.deleteFile}
|
|
251
|
+
aria-label={`${i18n.dropzone.deleteFile} ${file.name}`}
|
|
252
252
|
icon={<DeleteIcon />}
|
|
253
253
|
/>
|
|
254
254
|
</Transition>
|