react-dropzone 11.5.2 → 11.7.1

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/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 { fromEvent } from 'file-selector'
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(({ children, ...params }, ref) => {
41
- const { open, ...props } = useDropzone(params)
42
+ const Dropzone = forwardRef(({children, ...params}, ref) => {
43
+ const {open, ...props} = useDropzone(params)
42
44
 
43
- useImperativeHandle(ref, () => ({ open }), [open])
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({ ...props, open })}</Fragment>
48
+ return <Fragment>{children({...props, open})}</Fragment>
47
49
  })
48
50
 
49
51
  Dropzone.displayName = 'Dropzone'
@@ -61,7 +63,8 @@ const defaultProps = {
61
63
  noKeyboard: false,
62
64
  noDrag: false,
63
65
  noDragEventsBubbling: false,
64
- validator: null
66
+ validator: null,
67
+ useFsAccessApi: false,
65
68
  }
66
69
 
67
70
  Dropzone.defaultProps = defaultProps
@@ -158,11 +161,17 @@ Dropzone.propTypes = {
158
161
  */
159
162
  onFileDialogCancel: PropTypes.func,
160
163
 
161
- /**
162
- * Cb for when opening the file dialog
163
- */
164
+ /**
165
+ * Cb for when opening the file dialog
166
+ */
164
167
  onFileDialogOpen: PropTypes.func,
165
168
 
169
+ /**
170
+ * Set to true to use the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
171
+ * to open the file picker instead of using an `<input type="file">` click event.
172
+ */
173
+ useFsAccessApi: PropTypes.bool,
174
+
166
175
  /**
167
176
  * Cb for when the `dragenter` event occurs.
168
177
  *
@@ -359,6 +368,8 @@ const initialState = {
359
368
  * @param {boolean} [props.disabled=false] Enable/disable the dropzone
360
369
  * @param {getFilesFromEvent} [props.getFilesFromEvent] Use this to provide a custom file aggregator
361
370
  * @param {Function} [props.onFileDialogCancel] Cb for when closing the file dialog with no selection
371
+ * @param {boolean} [props.useFsAccessApi] Set to true to use the https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
372
+ * to open the file picker instead of using an `<input type="file">` click event.
362
373
  * @param {Function} [props.onFileDialogOpen] Cb for when opening the file dialog
363
374
  * @param {dragCb} [props.onDragEnter] Cb for when the `dragenter` event occurs.
364
375
  * @param {dragCb} [props.onDragLeave] Cb for when the `dragleave` event occurs
@@ -409,6 +420,7 @@ export function useDropzone(options = {}) {
409
420
  onDropRejected,
410
421
  onFileDialogCancel,
411
422
  onFileDialogOpen,
423
+ useFsAccessApi,
412
424
  preventDropOnDocument,
413
425
  noClick,
414
426
  noKeyboard,
@@ -420,23 +432,18 @@ export function useDropzone(options = {}) {
420
432
  ...options
421
433
  }
422
434
 
435
+ const onFileDialogOpenCb = useMemo(
436
+ () => typeof onFileDialogOpen === 'function' ? onFileDialogOpen : noop,
437
+ [onFileDialogOpen])
438
+ const onFileDialogCancelCb = useMemo(
439
+ () => typeof onFileDialogCancel === 'function' ? onFileDialogCancel : noop,
440
+ [onFileDialogCancel])
441
+
423
442
  const rootRef = useRef(null)
424
443
  const inputRef = useRef(null)
425
444
 
426
445
  const [state, dispatch] = useReducer(reducer, initialState)
427
- const { isFocused, isFileDialogActive, draggedFiles } = state
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])
446
+ const {isFocused, isFileDialogActive, draggedFiles} = state
440
447
 
441
448
  // Update file dialog active state when the window is focused on
442
449
  const onWindowFocus = () => {
@@ -444,65 +451,26 @@ export function useDropzone(options = {}) {
444
451
  if (isFileDialogActive) {
445
452
  setTimeout(() => {
446
453
  if (inputRef.current) {
447
- const { files } = inputRef.current
454
+ const {files} = inputRef.current
448
455
 
449
456
  if (!files.length) {
450
- dispatch({ type: 'closeDialog' })
451
-
452
- if (typeof onFileDialogCancel === 'function') {
453
- onFileDialogCancel()
454
- }
457
+ dispatch({type: 'closeDialog'})
458
+ onFileDialogCancelCb()
455
459
  }
456
460
  }
457
461
  }, 300)
458
462
  }
459
463
  }
460
464
  useEffect(() => {
465
+ if (useFsAccessApi && canUseFileSystemAccessAPI()) {
466
+ return () => {}
467
+ }
468
+
461
469
  window.addEventListener('focus', onWindowFocus, false)
462
470
  return () => {
463
471
  window.removeEventListener('focus', onWindowFocus, false)
464
472
  }
465
- }, [inputRef, isFileDialogActive, onFileDialogCancel])
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])
473
+ }, [inputRef, isFileDialogActive, onFileDialogCancelCb, useFsAccessApi])
506
474
 
507
475
  const dragTargetsRef = useRef([])
508
476
  const onDocumentDrop = event => {
@@ -614,6 +582,66 @@ export function useDropzone(options = {}) {
614
582
  [rootRef, onDragLeave, noDragEventsBubbling]
615
583
  )
616
584
 
585
+ const setFiles = useCallback((files, event) => {
586
+ const acceptedFiles = []
587
+ const fileRejections = []
588
+
589
+ files.forEach(file => {
590
+ const [accepted, acceptError] = fileAccepted(file, accept)
591
+ const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize)
592
+ const customErrors = validator ? validator(file) : null;
593
+
594
+ if (accepted && sizeMatch && !customErrors) {
595
+ acceptedFiles.push(file)
596
+ } else {
597
+ let errors = [acceptError, sizeError];
598
+
599
+ if (customErrors) {
600
+ errors = errors.concat(customErrors);
601
+ }
602
+
603
+ fileRejections.push({file, errors: errors.filter(e => e)})
604
+ }
605
+ })
606
+
607
+ if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) {
608
+ // Reject everything and empty accepted files
609
+ acceptedFiles.forEach(file => {
610
+ fileRejections.push({file, errors: [TOO_MANY_FILES_REJECTION]})
611
+ })
612
+ acceptedFiles.splice(0)
613
+ }
614
+
615
+ dispatch({
616
+ acceptedFiles,
617
+ fileRejections,
618
+ type: 'setFiles'
619
+ })
620
+
621
+ if (onDrop) {
622
+ onDrop(acceptedFiles, fileRejections, event)
623
+ }
624
+
625
+ if (fileRejections.length > 0 && onDropRejected) {
626
+ onDropRejected(fileRejections, event)
627
+ }
628
+
629
+ if (acceptedFiles.length > 0 && onDropAccepted) {
630
+ onDropAccepted(acceptedFiles, event)
631
+ }
632
+ }, [
633
+ dispatch,
634
+ multiple,
635
+ accept,
636
+ minSize,
637
+ maxSize,
638
+ maxFiles,
639
+ onDrop,
640
+ onDropAccepted,
641
+ onDropRejected,
642
+ validator
643
+ ]);
644
+
617
645
  const onDropCb = useCallback(
618
646
  event => {
619
647
  event.preventDefault()
@@ -628,72 +656,92 @@ export function useDropzone(options = {}) {
628
656
  if (isPropagationStopped(event) && !noDragEventsBubbling) {
629
657
  return
630
658
  }
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
- }
659
+ setFiles(files, event)
678
660
  })
679
661
  }
680
- dispatch({ type: 'reset' })
662
+ dispatch({type: 'reset'})
681
663
  },
682
664
  [
683
- multiple,
684
- accept,
685
- minSize,
686
- maxSize,
687
- maxFiles,
688
665
  getFilesFromEvent,
689
- onDrop,
690
- onDropAccepted,
691
- onDropRejected,
692
- noDragEventsBubbling,
693
- validator
666
+ setFiles,
667
+ noDragEventsBubbling
694
668
  ]
695
669
  )
696
670
 
671
+ // Fn for opening the file dialog programmatically
672
+ const openFileDialog = useCallback(() => {
673
+ if (useFsAccessApi && canUseFileSystemAccessAPI()) {
674
+ dispatch({type: 'openDialog'})
675
+ onFileDialogOpenCb()
676
+ // https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker
677
+ const opts = {
678
+ multiple,
679
+ types: filePickerOptionsTypes(accept)
680
+ };
681
+ window.showOpenFilePicker(opts)
682
+ .then(handles => getFilesFromEvent(handles))
683
+ .then(files => setFiles(files, null))
684
+ .catch(e => onFileDialogCancelCb(e))
685
+ .finally(() => dispatch({type: 'closeDialog'}));
686
+ return
687
+ }
688
+
689
+ if (inputRef.current) {
690
+ dispatch({type: 'openDialog'})
691
+ onFileDialogOpenCb()
692
+ inputRef.current.value = null
693
+ inputRef.current.click()
694
+ }
695
+ }, [
696
+ dispatch,
697
+ onFileDialogOpenCb,
698
+ onFileDialogCancelCb,
699
+ useFsAccessApi,
700
+ setFiles,
701
+ accept,
702
+ multiple
703
+ ])
704
+
705
+ // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone
706
+ const onKeyDownCb = useCallback(
707
+ event => {
708
+ // Ignore keyboard events bubbling up the DOM tree
709
+ if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) {
710
+ return
711
+ }
712
+
713
+ if (event.keyCode === 32 || event.keyCode === 13) {
714
+ event.preventDefault()
715
+ openFileDialog()
716
+ }
717
+ },
718
+ [rootRef, inputRef, openFileDialog]
719
+ )
720
+
721
+ // Update focus state for the dropzone
722
+ const onFocusCb = useCallback(() => {
723
+ dispatch({type: 'focus'})
724
+ }, [])
725
+ const onBlurCb = useCallback(() => {
726
+ dispatch({type: 'blur'})
727
+ }, [])
728
+
729
+ // Cb to open the file dialog when click occurs on the dropzone
730
+ const onClickCb = useCallback(() => {
731
+ if (noClick) {
732
+ return
733
+ }
734
+
735
+ // In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout()
736
+ // to ensure React can handle state changes
737
+ // See: https://github.com/react-dropzone/react-dropzone/issues/450
738
+ if (isIeOrEdge()) {
739
+ setTimeout(openFileDialog, 0)
740
+ } else {
741
+ openFileDialog()
742
+ }
743
+ }, [inputRef, noClick, openFileDialog])
744
+
697
745
  const composeHandler = fn => {
698
746
  return disabled ? null : fn
699
747
  }
@@ -715,6 +763,7 @@ export function useDropzone(options = {}) {
715
763
  const getRootProps = useMemo(
716
764
  () => ({
717
765
  refKey = 'ref',
766
+ role,
718
767
  onKeyDown,
719
768
  onFocus,
720
769
  onBlur,
@@ -733,8 +782,9 @@ export function useDropzone(options = {}) {
733
782
  onDragOver: composeDragHandler(composeEventHandlers(onDragOver, onDragOverCb)),
734
783
  onDragLeave: composeDragHandler(composeEventHandlers(onDragLeave, onDragLeaveCb)),
735
784
  onDrop: composeDragHandler(composeEventHandlers(onDrop, onDropCb)),
785
+ role: typeof role === 'string' && role !== '' ? role : 'button',
736
786
  [refKey]: rootRef,
737
- ...(!disabled && !noKeyboard ? { tabIndex: 0 } : {}),
787
+ ...(!disabled && !noKeyboard ? {tabIndex: 0} : {}),
738
788
  ...rest
739
789
  }),
740
790
  [
@@ -758,12 +808,12 @@ export function useDropzone(options = {}) {
758
808
  }, [])
759
809
 
760
810
  const getInputProps = useMemo(
761
- () => ({ refKey = 'ref', onChange, onClick, ...rest } = {}) => {
811
+ () => ({refKey = 'ref', onChange, onClick, ...rest} = {}) => {
762
812
  const inputProps = {
763
813
  accept,
764
814
  multiple,
765
815
  type: 'file',
766
- style: { display: 'none' },
816
+ style: {display: 'none'},
767
817
  onChange: composeHandler(composeEventHandlers(onChange, onDropCb)),
768
818
  onClick: composeHandler(composeEventHandlers(onClick, onInputElementClick)),
769
819
  autoComplete: 'off',
@@ -780,7 +830,7 @@ export function useDropzone(options = {}) {
780
830
  )
781
831
 
782
832
  const fileCount = draggedFiles.length
783
- const isDragAccept = fileCount > 0 && allFilesAccepted({ files: draggedFiles, accept, minSize, maxSize, multiple, maxFiles })
833
+ const isDragAccept = fileCount > 0 && allFilesAccepted({files: draggedFiles, accept, minSize, maxSize, multiple, maxFiles})
784
834
  const isDragReject = fileCount > 0 && !isDragAccept
785
835
 
786
836
  return {
@@ -811,7 +861,7 @@ function reducer(state, action) {
811
861
  }
812
862
  case 'openDialog':
813
863
  return {
814
- ...state,
864
+ ...initialState,
815
865
  isFileDialogActive: true
816
866
  }
817
867
  case 'closeDialog':
@@ -821,7 +871,7 @@ function reducer(state, action) {
821
871
  }
822
872
  case 'setDraggedFiles':
823
873
  /* eslint no-case-declarations: 0 */
824
- const { isDragActive, draggedFiles } = action
874
+ const {isDragActive, draggedFiles} = action
825
875
  return {
826
876
  ...state,
827
877
  draggedFiles,
@@ -842,4 +892,6 @@ function reducer(state, action) {
842
892
  }
843
893
  }
844
894
 
845
- export { ErrorCode } from './utils'
895
+ function noop() {}
896
+
897
+ export {ErrorCode} from './utils'