react-dropzone 11.5.1 → 11.7.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/.eslintignore +1 -3
- package/.eslintrc +1 -1
- package/.husky/commit-msg +5 -0
- package/.husky/pre-commit +5 -0
- package/.nvmrc +1 -1
- package/README.md +77 -29
- package/dist/es/index.js +157 -123
- package/dist/es/utils/index.js +39 -3
- package/dist/index.js +16 -2
- package/examples/events/README.md +3 -1
- package/examples/pintura/README.md +144 -0
- package/package.json +78 -75
- package/rollup.config.js +9 -6
- package/src/.eslintrc +7 -3
- package/src/__snapshots__/index.spec.js.snap +1 -1
- package/src/index.js +174 -133
- package/src/index.spec.js +240 -0
- package/src/utils/index.js +38 -4
- package/src/utils/index.spec.js +92 -29
- package/styleguide.config.js +2 -4
- package/typings/.eslintrc +28 -0
- package/examples/doka/README.md +0 -140
- package/tslint.json +0 -101
package/src/index.js
CHANGED
|
@@ -10,12 +10,14 @@ import React, {
|
|
|
10
10
|
useRef
|
|
11
11
|
} from 'react'
|
|
12
12
|
import PropTypes from 'prop-types'
|
|
13
|
-
import {
|
|
13
|
+
import {fromEvent} from 'file-selector'
|
|
14
14
|
import {
|
|
15
15
|
allFilesAccepted,
|
|
16
16
|
composeEventHandlers,
|
|
17
17
|
fileAccepted,
|
|
18
18
|
fileMatchSize,
|
|
19
|
+
filePickerOptionsTypes,
|
|
20
|
+
canUseFileSystemAccessAPI,
|
|
19
21
|
isEvtWithFiles,
|
|
20
22
|
isIeOrEdge,
|
|
21
23
|
isPropagationStopped,
|
|
@@ -37,13 +39,13 @@ import {
|
|
|
37
39
|
* </Dropzone>
|
|
38
40
|
* ```
|
|
39
41
|
*/
|
|
40
|
-
const Dropzone = forwardRef(({
|
|
41
|
-
const {
|
|
42
|
+
const Dropzone = forwardRef(({children, ...params}, ref) => {
|
|
43
|
+
const {open, ...props} = useDropzone(params)
|
|
42
44
|
|
|
43
|
-
useImperativeHandle(ref, () => ({
|
|
45
|
+
useImperativeHandle(ref, () => ({open}), [open])
|
|
44
46
|
|
|
45
47
|
// TODO: Figure out why react-styleguidist cannot create docs if we don't return a jsx element
|
|
46
|
-
return <Fragment>{children({
|
|
48
|
+
return <Fragment>{children({...props, open})}</Fragment>
|
|
47
49
|
})
|
|
48
50
|
|
|
49
51
|
Dropzone.displayName = 'Dropzone'
|
|
@@ -158,9 +160,9 @@ Dropzone.propTypes = {
|
|
|
158
160
|
*/
|
|
159
161
|
onFileDialogCancel: PropTypes.func,
|
|
160
162
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
/**
|
|
164
|
+
* Cb for when opening the file dialog
|
|
165
|
+
*/
|
|
164
166
|
onFileDialogOpen: PropTypes.func,
|
|
165
167
|
|
|
166
168
|
/**
|
|
@@ -420,23 +422,18 @@ export function useDropzone(options = {}) {
|
|
|
420
422
|
...options
|
|
421
423
|
}
|
|
422
424
|
|
|
425
|
+
const onFileDialogOpenCb = useMemo(
|
|
426
|
+
() => typeof onFileDialogOpen === 'function' ? onFileDialogOpen : noop,
|
|
427
|
+
[onFileDialogOpen])
|
|
428
|
+
const onFileDialogCancelCb = useMemo(
|
|
429
|
+
() => typeof onFileDialogCancel === 'function' ? onFileDialogCancel : noop,
|
|
430
|
+
[onFileDialogCancel])
|
|
431
|
+
|
|
423
432
|
const rootRef = useRef(null)
|
|
424
433
|
const inputRef = useRef(null)
|
|
425
434
|
|
|
426
435
|
const [state, dispatch] = useReducer(reducer, initialState)
|
|
427
|
-
const {
|
|
428
|
-
|
|
429
|
-
// Fn for opening the file dialog programmatically
|
|
430
|
-
const openFileDialog = useCallback(() => {
|
|
431
|
-
if (inputRef.current) {
|
|
432
|
-
dispatch({ type: 'openDialog' })
|
|
433
|
-
if (typeof onFileDialogOpen === 'function') {
|
|
434
|
-
onFileDialogOpen()
|
|
435
|
-
}
|
|
436
|
-
inputRef.current.value = null
|
|
437
|
-
inputRef.current.click()
|
|
438
|
-
}
|
|
439
|
-
}, [dispatch, onFileDialogOpen])
|
|
436
|
+
const {isFocused, isFileDialogActive, draggedFiles} = state
|
|
440
437
|
|
|
441
438
|
// Update file dialog active state when the window is focused on
|
|
442
439
|
const onWindowFocus = () => {
|
|
@@ -444,65 +441,26 @@ export function useDropzone(options = {}) {
|
|
|
444
441
|
if (isFileDialogActive) {
|
|
445
442
|
setTimeout(() => {
|
|
446
443
|
if (inputRef.current) {
|
|
447
|
-
const {
|
|
444
|
+
const {files} = inputRef.current
|
|
448
445
|
|
|
449
446
|
if (!files.length) {
|
|
450
|
-
dispatch({
|
|
451
|
-
|
|
452
|
-
if (typeof onFileDialogCancel === 'function') {
|
|
453
|
-
onFileDialogCancel()
|
|
454
|
-
}
|
|
447
|
+
dispatch({type: 'closeDialog'})
|
|
448
|
+
onFileDialogCancelCb()
|
|
455
449
|
}
|
|
456
450
|
}
|
|
457
451
|
}, 300)
|
|
458
452
|
}
|
|
459
453
|
}
|
|
460
454
|
useEffect(() => {
|
|
455
|
+
if (canUseFileSystemAccessAPI()) {
|
|
456
|
+
return () => {}
|
|
457
|
+
}
|
|
458
|
+
|
|
461
459
|
window.addEventListener('focus', onWindowFocus, false)
|
|
462
460
|
return () => {
|
|
463
461
|
window.removeEventListener('focus', onWindowFocus, false)
|
|
464
462
|
}
|
|
465
|
-
}, [inputRef, isFileDialogActive,
|
|
466
|
-
|
|
467
|
-
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
|
468
|
-
const onKeyDownCb = useCallback(
|
|
469
|
-
event => {
|
|
470
|
-
// Ignore keyboard events bubbling up the DOM tree
|
|
471
|
-
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) {
|
|
472
|
-
return
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (event.keyCode === 32 || event.keyCode === 13) {
|
|
476
|
-
event.preventDefault()
|
|
477
|
-
openFileDialog()
|
|
478
|
-
}
|
|
479
|
-
},
|
|
480
|
-
[rootRef, inputRef, openFileDialog]
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
// Update focus state for the dropzone
|
|
484
|
-
const onFocusCb = useCallback(() => {
|
|
485
|
-
dispatch({ type: 'focus' })
|
|
486
|
-
}, [])
|
|
487
|
-
const onBlurCb = useCallback(() => {
|
|
488
|
-
dispatch({ type: 'blur' })
|
|
489
|
-
}, [])
|
|
490
|
-
|
|
491
|
-
// Cb to open the file dialog when click occurs on the dropzone
|
|
492
|
-
const onClickCb = useCallback(() => {
|
|
493
|
-
if (noClick) {
|
|
494
|
-
return
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
|
|
498
|
-
// to ensure React can handle state changes
|
|
499
|
-
// See: https://github.com/react-dropzone/react-dropzone/issues/450
|
|
500
|
-
if (isIeOrEdge()) {
|
|
501
|
-
setTimeout(openFileDialog, 0)
|
|
502
|
-
} else {
|
|
503
|
-
openFileDialog()
|
|
504
|
-
}
|
|
505
|
-
}, [inputRef, noClick, openFileDialog])
|
|
463
|
+
}, [inputRef, isFileDialogActive, onFileDialogCancelCb])
|
|
506
464
|
|
|
507
465
|
const dragTargetsRef = useRef([])
|
|
508
466
|
const onDocumentDrop = event => {
|
|
@@ -614,6 +572,66 @@ export function useDropzone(options = {}) {
|
|
|
614
572
|
[rootRef, onDragLeave, noDragEventsBubbling]
|
|
615
573
|
)
|
|
616
574
|
|
|
575
|
+
const setFiles = useCallback((files, event) => {
|
|
576
|
+
const acceptedFiles = []
|
|
577
|
+
const fileRejections = []
|
|
578
|
+
|
|
579
|
+
files.forEach(file => {
|
|
580
|
+
const [accepted, acceptError] = fileAccepted(file, accept)
|
|
581
|
+
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize)
|
|
582
|
+
const customErrors = validator ? validator(file) : null;
|
|
583
|
+
|
|
584
|
+
if (accepted && sizeMatch && !customErrors) {
|
|
585
|
+
acceptedFiles.push(file)
|
|
586
|
+
} else {
|
|
587
|
+
let errors = [acceptError, sizeError];
|
|
588
|
+
|
|
589
|
+
if (customErrors) {
|
|
590
|
+
errors = errors.concat(customErrors);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
fileRejections.push({file, errors: errors.filter(e => e)})
|
|
594
|
+
}
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) {
|
|
598
|
+
// Reject everything and empty accepted files
|
|
599
|
+
acceptedFiles.forEach(file => {
|
|
600
|
+
fileRejections.push({file, errors: [TOO_MANY_FILES_REJECTION]})
|
|
601
|
+
})
|
|
602
|
+
acceptedFiles.splice(0)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
dispatch({
|
|
606
|
+
acceptedFiles,
|
|
607
|
+
fileRejections,
|
|
608
|
+
type: 'setFiles'
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
if (onDrop) {
|
|
612
|
+
onDrop(acceptedFiles, fileRejections, event)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (fileRejections.length > 0 && onDropRejected) {
|
|
616
|
+
onDropRejected(fileRejections, event)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (acceptedFiles.length > 0 && onDropAccepted) {
|
|
620
|
+
onDropAccepted(acceptedFiles, event)
|
|
621
|
+
}
|
|
622
|
+
}, [
|
|
623
|
+
dispatch,
|
|
624
|
+
multiple,
|
|
625
|
+
accept,
|
|
626
|
+
minSize,
|
|
627
|
+
maxSize,
|
|
628
|
+
maxFiles,
|
|
629
|
+
onDrop,
|
|
630
|
+
onDropAccepted,
|
|
631
|
+
onDropRejected,
|
|
632
|
+
validator
|
|
633
|
+
]);
|
|
634
|
+
|
|
617
635
|
const onDropCb = useCallback(
|
|
618
636
|
event => {
|
|
619
637
|
event.preventDefault()
|
|
@@ -628,72 +646,91 @@ export function useDropzone(options = {}) {
|
|
|
628
646
|
if (isPropagationStopped(event) && !noDragEventsBubbling) {
|
|
629
647
|
return
|
|
630
648
|
}
|
|
631
|
-
|
|
632
|
-
const acceptedFiles = []
|
|
633
|
-
const fileRejections = []
|
|
634
|
-
|
|
635
|
-
files.forEach(file => {
|
|
636
|
-
const [accepted, acceptError] = fileAccepted(file, accept)
|
|
637
|
-
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize)
|
|
638
|
-
const customErrors = validator ? validator(file) : null;
|
|
639
|
-
|
|
640
|
-
if (accepted && sizeMatch && !customErrors) {
|
|
641
|
-
acceptedFiles.push(file)
|
|
642
|
-
} else {
|
|
643
|
-
let errors = [acceptError, sizeError];
|
|
644
|
-
|
|
645
|
-
if (customErrors) {
|
|
646
|
-
errors = errors.concat(customErrors);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
fileRejections.push({ file, errors: errors.filter(e => e) })
|
|
650
|
-
}
|
|
651
|
-
})
|
|
652
|
-
|
|
653
|
-
if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) {
|
|
654
|
-
// Reject everything and empty accepted files
|
|
655
|
-
acceptedFiles.forEach(file => {
|
|
656
|
-
fileRejections.push({ file, errors: [TOO_MANY_FILES_REJECTION] })
|
|
657
|
-
})
|
|
658
|
-
acceptedFiles.splice(0)
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
dispatch({
|
|
662
|
-
acceptedFiles,
|
|
663
|
-
fileRejections,
|
|
664
|
-
type: 'setFiles'
|
|
665
|
-
})
|
|
666
|
-
|
|
667
|
-
if (onDrop) {
|
|
668
|
-
onDrop(acceptedFiles, fileRejections, event)
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
if (fileRejections.length > 0 && onDropRejected) {
|
|
672
|
-
onDropRejected(fileRejections, event)
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
if (acceptedFiles.length > 0 && onDropAccepted) {
|
|
676
|
-
onDropAccepted(acceptedFiles, event)
|
|
677
|
-
}
|
|
649
|
+
setFiles(files, event)
|
|
678
650
|
})
|
|
679
651
|
}
|
|
680
|
-
dispatch({
|
|
652
|
+
dispatch({type: 'reset'})
|
|
681
653
|
},
|
|
682
654
|
[
|
|
683
|
-
multiple,
|
|
684
|
-
accept,
|
|
685
|
-
minSize,
|
|
686
|
-
maxSize,
|
|
687
|
-
maxFiles,
|
|
688
655
|
getFilesFromEvent,
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
onDropRejected,
|
|
692
|
-
noDragEventsBubbling,
|
|
693
|
-
validator
|
|
656
|
+
setFiles,
|
|
657
|
+
noDragEventsBubbling
|
|
694
658
|
]
|
|
695
659
|
)
|
|
696
660
|
|
|
661
|
+
// Fn for opening the file dialog programmatically
|
|
662
|
+
const openFileDialog = useCallback(() => {
|
|
663
|
+
if (canUseFileSystemAccessAPI()) {
|
|
664
|
+
dispatch({type: 'openDialog'})
|
|
665
|
+
onFileDialogOpenCb()
|
|
666
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
|
|
667
|
+
const opts = {
|
|
668
|
+
multiple,
|
|
669
|
+
types: filePickerOptionsTypes(accept)
|
|
670
|
+
};
|
|
671
|
+
window.showOpenFilePicker(opts)
|
|
672
|
+
.then(handles => getFilesFromEvent(handles))
|
|
673
|
+
.then(files => setFiles(files, null))
|
|
674
|
+
.catch(e => onFileDialogCancelCb(e))
|
|
675
|
+
.finally(() => dispatch({type: 'closeDialog'}));
|
|
676
|
+
return
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (inputRef.current) {
|
|
680
|
+
dispatch({type: 'openDialog'})
|
|
681
|
+
onFileDialogOpenCb()
|
|
682
|
+
inputRef.current.value = null
|
|
683
|
+
inputRef.current.click()
|
|
684
|
+
}
|
|
685
|
+
}, [
|
|
686
|
+
dispatch,
|
|
687
|
+
onFileDialogOpenCb,
|
|
688
|
+
onFileDialogCancelCb,
|
|
689
|
+
setFiles,
|
|
690
|
+
accept,
|
|
691
|
+
multiple
|
|
692
|
+
])
|
|
693
|
+
|
|
694
|
+
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
|
|
695
|
+
const onKeyDownCb = useCallback(
|
|
696
|
+
event => {
|
|
697
|
+
// Ignore keyboard events bubbling up the DOM tree
|
|
698
|
+
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) {
|
|
699
|
+
return
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (event.keyCode === 32 || event.keyCode === 13) {
|
|
703
|
+
event.preventDefault()
|
|
704
|
+
openFileDialog()
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
[rootRef, inputRef, openFileDialog]
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
// Update focus state for the dropzone
|
|
711
|
+
const onFocusCb = useCallback(() => {
|
|
712
|
+
dispatch({type: 'focus'})
|
|
713
|
+
}, [])
|
|
714
|
+
const onBlurCb = useCallback(() => {
|
|
715
|
+
dispatch({type: 'blur'})
|
|
716
|
+
}, [])
|
|
717
|
+
|
|
718
|
+
// Cb to open the file dialog when click occurs on the dropzone
|
|
719
|
+
const onClickCb = useCallback(() => {
|
|
720
|
+
if (noClick) {
|
|
721
|
+
return
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
|
|
725
|
+
// to ensure React can handle state changes
|
|
726
|
+
// See: https://github.com/react-dropzone/react-dropzone/issues/450
|
|
727
|
+
if (isIeOrEdge()) {
|
|
728
|
+
setTimeout(openFileDialog, 0)
|
|
729
|
+
} else {
|
|
730
|
+
openFileDialog()
|
|
731
|
+
}
|
|
732
|
+
}, [inputRef, noClick, openFileDialog])
|
|
733
|
+
|
|
697
734
|
const composeHandler = fn => {
|
|
698
735
|
return disabled ? null : fn
|
|
699
736
|
}
|
|
@@ -715,6 +752,7 @@ export function useDropzone(options = {}) {
|
|
|
715
752
|
const getRootProps = useMemo(
|
|
716
753
|
() => ({
|
|
717
754
|
refKey = 'ref',
|
|
755
|
+
role,
|
|
718
756
|
onKeyDown,
|
|
719
757
|
onFocus,
|
|
720
758
|
onBlur,
|
|
@@ -733,8 +771,9 @@ export function useDropzone(options = {}) {
|
|
|
733
771
|
onDragOver: composeDragHandler(composeEventHandlers(onDragOver, onDragOverCb)),
|
|
734
772
|
onDragLeave: composeDragHandler(composeEventHandlers(onDragLeave, onDragLeaveCb)),
|
|
735
773
|
onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),
|
|
774
|
+
role: typeof role === 'string' && role !== '' ? role : 'button',
|
|
736
775
|
[refKey]: rootRef,
|
|
737
|
-
...(!disabled && !noKeyboard ? {
|
|
776
|
+
...(!disabled && !noKeyboard ? {tabIndex: 0} : {}),
|
|
738
777
|
...rest
|
|
739
778
|
}),
|
|
740
779
|
[
|
|
@@ -758,12 +797,12 @@ export function useDropzone(options = {}) {
|
|
|
758
797
|
}, [])
|
|
759
798
|
|
|
760
799
|
const getInputProps = useMemo(
|
|
761
|
-
() => ({
|
|
800
|
+
() => ({refKey = 'ref', onChange, onClick, ...rest} = {}) => {
|
|
762
801
|
const inputProps = {
|
|
763
802
|
accept,
|
|
764
803
|
multiple,
|
|
765
804
|
type: 'file',
|
|
766
|
-
style: {
|
|
805
|
+
style: {display: 'none'},
|
|
767
806
|
onChange: composeHandler(composeEventHandlers(onChange, onDropCb)),
|
|
768
807
|
onClick: composeHandler(composeEventHandlers(onClick, onInputElementClick)),
|
|
769
808
|
autoComplete: 'off',
|
|
@@ -780,7 +819,7 @@ export function useDropzone(options = {}) {
|
|
|
780
819
|
)
|
|
781
820
|
|
|
782
821
|
const fileCount = draggedFiles.length
|
|
783
|
-
const isDragAccept = fileCount > 0 && allFilesAccepted({
|
|
822
|
+
const isDragAccept = fileCount > 0 && allFilesAccepted({files: draggedFiles, accept, minSize, maxSize, multiple, maxFiles})
|
|
784
823
|
const isDragReject = fileCount > 0 && !isDragAccept
|
|
785
824
|
|
|
786
825
|
return {
|
|
@@ -811,7 +850,7 @@ function reducer(state, action) {
|
|
|
811
850
|
}
|
|
812
851
|
case 'openDialog':
|
|
813
852
|
return {
|
|
814
|
-
...
|
|
853
|
+
...initialState,
|
|
815
854
|
isFileDialogActive: true
|
|
816
855
|
}
|
|
817
856
|
case 'closeDialog':
|
|
@@ -821,7 +860,7 @@ function reducer(state, action) {
|
|
|
821
860
|
}
|
|
822
861
|
case 'setDraggedFiles':
|
|
823
862
|
/* eslint no-case-declarations: 0 */
|
|
824
|
-
const {
|
|
863
|
+
const {isDragActive, draggedFiles} = action
|
|
825
864
|
return {
|
|
826
865
|
...state,
|
|
827
866
|
draggedFiles,
|
|
@@ -842,4 +881,6 @@ function reducer(state, action) {
|
|
|
842
881
|
}
|
|
843
882
|
}
|
|
844
883
|
|
|
845
|
-
|
|
884
|
+
function noop() {}
|
|
885
|
+
|
|
886
|
+
export {ErrorCode} from './utils'
|
package/src/index.spec.js
CHANGED
|
@@ -1159,6 +1159,20 @@ describe('useDropzone() hook', () => {
|
|
|
1159
1159
|
})
|
|
1160
1160
|
|
|
1161
1161
|
describe('onClick', () => {
|
|
1162
|
+
let currentShowOpenFilePicker;
|
|
1163
|
+
|
|
1164
|
+
beforeEach(() => {
|
|
1165
|
+
currentShowOpenFilePicker = window.showOpenFilePicker
|
|
1166
|
+
})
|
|
1167
|
+
|
|
1168
|
+
afterEach(() => {
|
|
1169
|
+
if (currentShowOpenFilePicker) {
|
|
1170
|
+
window.showOpenFilePicker = currentShowOpenFilePicker
|
|
1171
|
+
} else {
|
|
1172
|
+
delete window.showOpenFilePicker
|
|
1173
|
+
}
|
|
1174
|
+
})
|
|
1175
|
+
|
|
1162
1176
|
it('should proxy the click event to the input', () => {
|
|
1163
1177
|
const activeRef = createRef()
|
|
1164
1178
|
const active = <span ref={activeRef}>I am active</span>
|
|
@@ -1303,6 +1317,185 @@ describe('useDropzone() hook', () => {
|
|
|
1303
1317
|
jest.useRealTimers()
|
|
1304
1318
|
isIeOrEdgeSpy.mockClear()
|
|
1305
1319
|
})
|
|
1320
|
+
|
|
1321
|
+
it('should use showOpenFilePicker() if supported and not trigger click on input', async () => {
|
|
1322
|
+
const activeRef = createRef()
|
|
1323
|
+
const active = <span ref={activeRef}>I am active</span>
|
|
1324
|
+
const onClickSpy = jest.spyOn(HTMLInputElement.prototype, 'click')
|
|
1325
|
+
|
|
1326
|
+
const handlers = files.map(f => createFileSystemFileHandle(f))
|
|
1327
|
+
const thenable = createThenable()
|
|
1328
|
+
const showOpenFilePickerMock = jest.fn().mockReturnValue(thenable.promise)
|
|
1329
|
+
|
|
1330
|
+
window.showOpenFilePicker = showOpenFilePickerMock
|
|
1331
|
+
|
|
1332
|
+
const onDropSpy = jest.fn()
|
|
1333
|
+
const onFileDialogOpenSpy = jest.fn()
|
|
1334
|
+
|
|
1335
|
+
const ui = (
|
|
1336
|
+
<Dropzone
|
|
1337
|
+
onDrop={onDropSpy}
|
|
1338
|
+
onFileDialogOpen={onFileDialogOpenSpy}
|
|
1339
|
+
accept="application/pdf"
|
|
1340
|
+
multiple
|
|
1341
|
+
>
|
|
1342
|
+
{({ getRootProps, getInputProps, isFileDialogActive }) => (
|
|
1343
|
+
<div {...getRootProps()}>
|
|
1344
|
+
<input {...getInputProps()} />
|
|
1345
|
+
{isFileDialogActive && active}
|
|
1346
|
+
</div>
|
|
1347
|
+
)}
|
|
1348
|
+
</Dropzone>
|
|
1349
|
+
)
|
|
1350
|
+
|
|
1351
|
+
const { container, rerender } = render(ui)
|
|
1352
|
+
|
|
1353
|
+
const dropzone = container.querySelector('div')
|
|
1354
|
+
|
|
1355
|
+
fireEvent.click(dropzone)
|
|
1356
|
+
|
|
1357
|
+
await flushPromises(rerender, ui)
|
|
1358
|
+
|
|
1359
|
+
expect(activeRef.current).not.toBeNull()
|
|
1360
|
+
expect(dropzone).toContainElement(activeRef.current)
|
|
1361
|
+
expect(onFileDialogOpenSpy).toHaveBeenCalled()
|
|
1362
|
+
|
|
1363
|
+
thenable.done(handlers)
|
|
1364
|
+
await flushPromises(rerender, ui)
|
|
1365
|
+
|
|
1366
|
+
expect(activeRef.current).toBeNull() // We expect the dialog to be closed at this point
|
|
1367
|
+
expect(dropzone).not.toContainElement(activeRef.current)
|
|
1368
|
+
expect(onClickSpy).not.toHaveBeenCalled()
|
|
1369
|
+
expect(showOpenFilePickerMock).toHaveBeenCalledWith({
|
|
1370
|
+
multiple: true,
|
|
1371
|
+
types: [{
|
|
1372
|
+
description: 'everything',
|
|
1373
|
+
accept: {'application/pdf': []}
|
|
1374
|
+
}]
|
|
1375
|
+
})
|
|
1376
|
+
expect(onDropSpy).toHaveBeenCalledWith(files, [], null)
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
test('if showOpenFilePicker() is supported it should work without the <input>', async () => {
|
|
1380
|
+
const activeRef = createRef()
|
|
1381
|
+
const active = <span ref={activeRef}>I am active</span>
|
|
1382
|
+
|
|
1383
|
+
const handlers = files.map(f => createFileSystemFileHandle(f))
|
|
1384
|
+
const showOpenFilePickerMock = jest.fn().mockReturnValue(Promise.resolve(handlers))
|
|
1385
|
+
|
|
1386
|
+
window.showOpenFilePicker = showOpenFilePickerMock
|
|
1387
|
+
|
|
1388
|
+
const onDropSpy = jest.fn()
|
|
1389
|
+
const onFileDialogOpenSpy = jest.fn()
|
|
1390
|
+
|
|
1391
|
+
const ui = (
|
|
1392
|
+
<Dropzone onDrop={onDropSpy} onFileDialogOpen={onFileDialogOpenSpy}>
|
|
1393
|
+
{({ getRootProps, isFileDialogActive }) => (
|
|
1394
|
+
<div {...getRootProps()}>
|
|
1395
|
+
{isFileDialogActive && active}
|
|
1396
|
+
</div>
|
|
1397
|
+
)}
|
|
1398
|
+
</Dropzone>
|
|
1399
|
+
)
|
|
1400
|
+
|
|
1401
|
+
const { container, rerender } = render(ui)
|
|
1402
|
+
|
|
1403
|
+
const dropzone = container.querySelector('div')
|
|
1404
|
+
|
|
1405
|
+
fireEvent.click(dropzone)
|
|
1406
|
+
await flushPromises(rerender, ui)
|
|
1407
|
+
const ref = activeRef.current
|
|
1408
|
+
expect(ref).toBeNull() // We expect the dialog to be closed at this point
|
|
1409
|
+
expect(dropzone).not.toContainElement(ref)
|
|
1410
|
+
expect(showOpenFilePickerMock).toHaveBeenCalled()
|
|
1411
|
+
expect(onDropSpy).toHaveBeenCalledWith(files, [], null)
|
|
1412
|
+
expect(onFileDialogOpenSpy).toHaveBeenCalled()
|
|
1413
|
+
})
|
|
1414
|
+
|
|
1415
|
+
test('if showOpenFilePicker() is supported and the user cancels it should call onFileDialogCancel', async () => {
|
|
1416
|
+
const activeRef = createRef()
|
|
1417
|
+
const active = <span ref={activeRef}>I am active</span>
|
|
1418
|
+
|
|
1419
|
+
const showOpenFilePickerMock = jest.fn().mockReturnValue(Promise.reject(new DOMException("user aborted request", "AbortError")))
|
|
1420
|
+
|
|
1421
|
+
window.showOpenFilePicker = showOpenFilePickerMock
|
|
1422
|
+
|
|
1423
|
+
const onDropSpy = jest.fn()
|
|
1424
|
+
const onFileDialogCancelSpy = jest.fn()
|
|
1425
|
+
|
|
1426
|
+
const ui = (
|
|
1427
|
+
<Dropzone onDrop={onDropSpy} onFileDialogCancel={onFileDialogCancelSpy}>
|
|
1428
|
+
{({ getRootProps, isFileDialogActive }) => (
|
|
1429
|
+
<div {...getRootProps()}>
|
|
1430
|
+
{isFileDialogActive && active}
|
|
1431
|
+
</div>
|
|
1432
|
+
)}
|
|
1433
|
+
</Dropzone>
|
|
1434
|
+
)
|
|
1435
|
+
|
|
1436
|
+
const { container, rerender } = render(ui)
|
|
1437
|
+
|
|
1438
|
+
const dropzone = container.querySelector('div')
|
|
1439
|
+
|
|
1440
|
+
fireEvent.click(dropzone)
|
|
1441
|
+
await flushPromises(rerender, ui)
|
|
1442
|
+
const ref = activeRef.current
|
|
1443
|
+
expect(ref).toBeNull() // We expect the dialog to be closed at this point
|
|
1444
|
+
expect(dropzone).not.toContainElement(ref)
|
|
1445
|
+
expect(showOpenFilePickerMock).toHaveBeenCalled()
|
|
1446
|
+
expect(onDropSpy).not.toHaveBeenCalled()
|
|
1447
|
+
expect(onFileDialogCancelSpy).toHaveBeenCalled()
|
|
1448
|
+
})
|
|
1449
|
+
|
|
1450
|
+
test('window focus evt is not bound if showOpenFilePicker() is supported', async () => {
|
|
1451
|
+
jest.useFakeTimers()
|
|
1452
|
+
|
|
1453
|
+
const activeRef = createRef()
|
|
1454
|
+
const active = <span ref={activeRef}>I am active</span>
|
|
1455
|
+
const onFileDialogCancelSpy = jest.fn()
|
|
1456
|
+
|
|
1457
|
+
const thenable = createThenable()
|
|
1458
|
+
const showOpenFilePickerMock = jest.fn().mockReturnValue(thenable.promise)
|
|
1459
|
+
|
|
1460
|
+
window.showOpenFilePicker = showOpenFilePickerMock
|
|
1461
|
+
|
|
1462
|
+
const ui = (
|
|
1463
|
+
<Dropzone onFileDialogCancel={onFileDialogCancelSpy} noClick>
|
|
1464
|
+
{({ getRootProps, isFileDialogActive, open }) => (
|
|
1465
|
+
<div {...getRootProps()}>
|
|
1466
|
+
{isFileDialogActive && active}
|
|
1467
|
+
<button type="button" onClick={open}>
|
|
1468
|
+
Open
|
|
1469
|
+
</button>
|
|
1470
|
+
</div>
|
|
1471
|
+
)}
|
|
1472
|
+
</Dropzone>
|
|
1473
|
+
)
|
|
1474
|
+
|
|
1475
|
+
const { container, rerender } = render(ui)
|
|
1476
|
+
|
|
1477
|
+
const dropzone = container.querySelector('div')
|
|
1478
|
+
const btn = container.querySelector('button')
|
|
1479
|
+
|
|
1480
|
+
btn.click()
|
|
1481
|
+
drainTimers()
|
|
1482
|
+
await flushPromises(rerender, ui)
|
|
1483
|
+
|
|
1484
|
+
const ref = activeRef.current
|
|
1485
|
+
expect(ref).not.toBeNull()
|
|
1486
|
+
expect(dropzone).toContainElement(ref)
|
|
1487
|
+
|
|
1488
|
+
thenable.cancel(new DOMException('user aborted request', 'AbortError'))
|
|
1489
|
+
|
|
1490
|
+
dispatchEvt(document.body, 'focus')
|
|
1491
|
+
drainTimers()
|
|
1492
|
+
await flushPromises(rerender, ui)
|
|
1493
|
+
|
|
1494
|
+
expect(onFileDialogCancelSpy).toHaveBeenCalledTimes(1)
|
|
1495
|
+
expect(dropzone).not.toContainElement(ref)
|
|
1496
|
+
|
|
1497
|
+
jest.useRealTimers()
|
|
1498
|
+
})
|
|
1306
1499
|
})
|
|
1307
1500
|
|
|
1308
1501
|
describe('onKeyDown', () => {
|
|
@@ -2803,6 +2996,34 @@ describe('useDropzone() hook', () => {
|
|
|
2803
2996
|
], expect.anything())
|
|
2804
2997
|
})
|
|
2805
2998
|
})
|
|
2999
|
+
|
|
3000
|
+
describe('accessibility', () => {
|
|
3001
|
+
it('sets the role attribute to button by default on the root', () => {
|
|
3002
|
+
const { container } = render(
|
|
3003
|
+
<Dropzone>
|
|
3004
|
+
{({ getRootProps }) => (
|
|
3005
|
+
<div id="root" {...getRootProps()} />
|
|
3006
|
+
)}
|
|
3007
|
+
</Dropzone>
|
|
3008
|
+
)
|
|
3009
|
+
const root = container.querySelector('#root')
|
|
3010
|
+
|
|
3011
|
+
expect(root).toHaveAttribute('role', 'button')
|
|
3012
|
+
})
|
|
3013
|
+
|
|
3014
|
+
test('users can override the default role attribute on the root', () => {
|
|
3015
|
+
const { container } = render(
|
|
3016
|
+
<Dropzone>
|
|
3017
|
+
{({ getRootProps }) => (
|
|
3018
|
+
<div id="root" {...getRootProps({role: 'generic'})} />
|
|
3019
|
+
)}
|
|
3020
|
+
</Dropzone>
|
|
3021
|
+
)
|
|
3022
|
+
const root = container.querySelector('#root')
|
|
3023
|
+
|
|
3024
|
+
expect(root).toHaveAttribute('role', 'generic')
|
|
3025
|
+
})
|
|
3026
|
+
})
|
|
2806
3027
|
})
|
|
2807
3028
|
|
|
2808
3029
|
async function flushPromises(rerender, ui) {
|
|
@@ -2854,6 +3075,10 @@ function dispatchEvt(node, type, data) {
|
|
|
2854
3075
|
fireEvent(node, event)
|
|
2855
3076
|
}
|
|
2856
3077
|
|
|
3078
|
+
function createFileSystemFileHandle(file) {
|
|
3079
|
+
return {getFile: () => Promise.resolve(file)}
|
|
3080
|
+
}
|
|
3081
|
+
|
|
2857
3082
|
function createFile(name, size, type) {
|
|
2858
3083
|
const file = new File([], name, { type })
|
|
2859
3084
|
Object.defineProperty(file, 'size', {
|
|
@@ -2863,3 +3088,18 @@ function createFile(name, size, type) {
|
|
|
2863
3088
|
})
|
|
2864
3089
|
return file
|
|
2865
3090
|
}
|
|
3091
|
+
|
|
3092
|
+
function createThenable() {
|
|
3093
|
+
let done, cancel;
|
|
3094
|
+
|
|
3095
|
+
const promise = new Promise((resolve, reject) => {
|
|
3096
|
+
done = resolve
|
|
3097
|
+
cancel = reject
|
|
3098
|
+
})
|
|
3099
|
+
|
|
3100
|
+
return {
|
|
3101
|
+
promise,
|
|
3102
|
+
done,
|
|
3103
|
+
cancel
|
|
3104
|
+
}
|
|
3105
|
+
}
|