react-dropzone
Advanced tools
Comparing version 11.5.3 to 11.6.0
@@ -26,5 +26,5 @@ var _excluded = ["children"], | ||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; } | ||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } | ||
@@ -41,3 +41,3 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
import { fromEvent } from 'file-selector'; | ||
import { allFilesAccepted, composeEventHandlers, fileAccepted, fileMatchSize, isEvtWithFiles, isIeOrEdge, isPropagationStopped, onDocumentDragOver, TOO_MANY_FILES_REJECTION } from './utils/index'; | ||
import { allFilesAccepted, composeEventHandlers, fileAccepted, fileMatchSize, filePickerOptionsTypes, canUseFileSystemAccessAPI, isEvtWithFiles, isIeOrEdge, isPropagationStopped, onDocumentDragOver, TOO_MANY_FILES_REJECTION } from './utils/index'; | ||
/** | ||
@@ -444,2 +444,8 @@ * Convenience wrapper component for the `useDropzone` hook | ||
var onFileDialogOpenCb = useMemo(function () { | ||
return typeof onFileDialogOpen === 'function' ? onFileDialogOpen : noop; | ||
}, [onFileDialogOpen]); | ||
var onFileDialogCancelCb = useMemo(function () { | ||
return typeof onFileDialogCancel === 'function' ? onFileDialogCancel : noop; | ||
}, [onFileDialogCancel]); | ||
var rootRef = useRef(null); | ||
@@ -455,19 +461,4 @@ var inputRef = useRef(null); | ||
isFileDialogActive = state.isFileDialogActive, | ||
draggedFiles = state.draggedFiles; // Fn for opening the file dialog programmatically | ||
draggedFiles = state.draggedFiles; // Update file dialog active state when the window is focused on | ||
var openFileDialog = useCallback(function () { | ||
if (inputRef.current) { | ||
dispatch({ | ||
type: 'openDialog' | ||
}); | ||
if (typeof onFileDialogOpen === 'function') { | ||
onFileDialogOpen(); | ||
} | ||
inputRef.current.value = null; | ||
inputRef.current.click(); | ||
} | ||
}, [dispatch, onFileDialogOpen]); // Update file dialog active state when the window is focused on | ||
var onWindowFocus = function onWindowFocus() { | ||
@@ -484,6 +475,3 @@ // Execute the timeout only if the file dialog is opened in the browser | ||
}); | ||
if (typeof onFileDialogCancel === 'function') { | ||
onFileDialogCancel(); | ||
} | ||
onFileDialogCancelCb(); | ||
} | ||
@@ -496,2 +484,6 @@ } | ||
useEffect(function () { | ||
if (canUseFileSystemAccessAPI()) { | ||
return function () {}; | ||
} | ||
window.addEventListener('focus', onWindowFocus, false); | ||
@@ -501,41 +493,3 @@ return function () { | ||
}; | ||
}, [inputRef, isFileDialogActive, onFileDialogCancel]); // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone | ||
var onKeyDownCb = useCallback(function (event) { | ||
// Ignore keyboard events bubbling up the DOM tree | ||
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { | ||
return; | ||
} | ||
if (event.keyCode === 32 || event.keyCode === 13) { | ||
event.preventDefault(); | ||
openFileDialog(); | ||
} | ||
}, [rootRef, inputRef, openFileDialog]); // Update focus state for the dropzone | ||
var onFocusCb = useCallback(function () { | ||
dispatch({ | ||
type: 'focus' | ||
}); | ||
}, []); | ||
var onBlurCb = useCallback(function () { | ||
dispatch({ | ||
type: 'blur' | ||
}); | ||
}, []); // Cb to open the file dialog when click occurs on the dropzone | ||
var onClickCb = useCallback(function () { | ||
if (noClick) { | ||
return; | ||
} // In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout() | ||
// to ensure React can handle state changes | ||
// See: https://github.com/react-dropzone/react-dropzone/issues/450 | ||
if (isIeOrEdge()) { | ||
setTimeout(openFileDialog, 0); | ||
} else { | ||
openFileDialog(); | ||
} | ||
}, [inputRef, noClick, openFileDialog]); | ||
}, [inputRef, isFileDialogActive, onFileDialogCancelCb]); | ||
var dragTargetsRef = useRef([]); | ||
@@ -643,2 +597,65 @@ | ||
}, [rootRef, onDragLeave, noDragEventsBubbling]); | ||
var setFiles = useCallback(function (files, event) { | ||
var acceptedFiles = []; | ||
var fileRejections = []; | ||
files.forEach(function (file) { | ||
var _fileAccepted = fileAccepted(file, accept), | ||
_fileAccepted2 = _slicedToArray(_fileAccepted, 2), | ||
accepted = _fileAccepted2[0], | ||
acceptError = _fileAccepted2[1]; | ||
var _fileMatchSize = fileMatchSize(file, minSize, maxSize), | ||
_fileMatchSize2 = _slicedToArray(_fileMatchSize, 2), | ||
sizeMatch = _fileMatchSize2[0], | ||
sizeError = _fileMatchSize2[1]; | ||
var customErrors = validator ? validator(file) : null; | ||
if (accepted && sizeMatch && !customErrors) { | ||
acceptedFiles.push(file); | ||
} else { | ||
var errors = [acceptError, sizeError]; | ||
if (customErrors) { | ||
errors = errors.concat(customErrors); | ||
} | ||
fileRejections.push({ | ||
file: file, | ||
errors: errors.filter(function (e) { | ||
return e; | ||
}) | ||
}); | ||
} | ||
}); | ||
if (!multiple && acceptedFiles.length > 1 || multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles) { | ||
// Reject everything and empty accepted files | ||
acceptedFiles.forEach(function (file) { | ||
fileRejections.push({ | ||
file: file, | ||
errors: [TOO_MANY_FILES_REJECTION] | ||
}); | ||
}); | ||
acceptedFiles.splice(0); | ||
} | ||
dispatch({ | ||
acceptedFiles: acceptedFiles, | ||
fileRejections: fileRejections, | ||
type: 'setFiles' | ||
}); | ||
if (onDrop) { | ||
onDrop(acceptedFiles, fileRejections, event); | ||
} | ||
if (fileRejections.length > 0 && onDropRejected) { | ||
onDropRejected(fileRejections, event); | ||
} | ||
if (acceptedFiles.length > 0 && onDropAccepted) { | ||
onDropAccepted(acceptedFiles, event); | ||
} | ||
}, [dispatch, multiple, accept, minSize, maxSize, maxFiles, onDrop, onDropAccepted, onDropRejected, validator]); | ||
var onDropCb = useCallback(function (event) { | ||
@@ -657,71 +674,84 @@ event.preventDefault(); // Persist here because we need the event later after getFilesFromEvent() is done | ||
var acceptedFiles = []; | ||
var fileRejections = []; | ||
files.forEach(function (file) { | ||
var _fileAccepted = fileAccepted(file, accept), | ||
_fileAccepted2 = _slicedToArray(_fileAccepted, 2), | ||
accepted = _fileAccepted2[0], | ||
acceptError = _fileAccepted2[1]; | ||
setFiles(files, event); | ||
}); | ||
} | ||
var _fileMatchSize = fileMatchSize(file, minSize, maxSize), | ||
_fileMatchSize2 = _slicedToArray(_fileMatchSize, 2), | ||
sizeMatch = _fileMatchSize2[0], | ||
sizeError = _fileMatchSize2[1]; | ||
dispatch({ | ||
type: 'reset' | ||
}); | ||
}, [getFilesFromEvent, setFiles, noDragEventsBubbling]); // Fn for opening the file dialog programmatically | ||
var customErrors = validator ? validator(file) : null; | ||
var openFileDialog = useCallback(function () { | ||
if (canUseFileSystemAccessAPI()) { | ||
dispatch({ | ||
type: 'openDialog' | ||
}); | ||
onFileDialogOpenCb(); // https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker | ||
if (accepted && sizeMatch && !customErrors) { | ||
acceptedFiles.push(file); | ||
} else { | ||
var errors = [acceptError, sizeError]; | ||
var opts = { | ||
multiple: multiple, | ||
types: filePickerOptionsTypes(accept) | ||
}; | ||
window.showOpenFilePicker(opts).then(function (handles) { | ||
return getFilesFromEvent(handles); | ||
}).then(function (files) { | ||
return setFiles(files, null); | ||
}).catch(function (e) { | ||
return onFileDialogCancelCb(e); | ||
}).finally(function () { | ||
return dispatch({ | ||
type: 'closeDialog' | ||
}); | ||
}); | ||
return; | ||
} | ||
if (customErrors) { | ||
errors = errors.concat(customErrors); | ||
} | ||
if (inputRef.current) { | ||
dispatch({ | ||
type: 'openDialog' | ||
}); | ||
onFileDialogOpenCb(); | ||
inputRef.current.value = null; | ||
inputRef.current.click(); | ||
} | ||
}, [dispatch, onFileDialogOpenCb, onFileDialogCancelCb, setFiles, accept, multiple]); // Cb to open the file dialog when SPACE/ENTER occurs on the dropzone | ||
fileRejections.push({ | ||
file: file, | ||
errors: errors.filter(function (e) { | ||
return e; | ||
}) | ||
}); | ||
} | ||
}); | ||
var onKeyDownCb = useCallback(function (event) { | ||
// Ignore keyboard events bubbling up the DOM tree | ||
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { | ||
return; | ||
} | ||
if (!multiple && acceptedFiles.length > 1 || multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles) { | ||
// Reject everything and empty accepted files | ||
acceptedFiles.forEach(function (file) { | ||
fileRejections.push({ | ||
file: file, | ||
errors: [TOO_MANY_FILES_REJECTION] | ||
}); | ||
}); | ||
acceptedFiles.splice(0); | ||
} | ||
if (event.keyCode === 32 || event.keyCode === 13) { | ||
event.preventDefault(); | ||
openFileDialog(); | ||
} | ||
}, [rootRef, inputRef, openFileDialog]); // Update focus state for the dropzone | ||
dispatch({ | ||
acceptedFiles: acceptedFiles, | ||
fileRejections: fileRejections, | ||
type: 'setFiles' | ||
}); | ||
var onFocusCb = useCallback(function () { | ||
dispatch({ | ||
type: 'focus' | ||
}); | ||
}, []); | ||
var onBlurCb = useCallback(function () { | ||
dispatch({ | ||
type: 'blur' | ||
}); | ||
}, []); // Cb to open the file dialog when click occurs on the dropzone | ||
if (onDrop) { | ||
onDrop(acceptedFiles, fileRejections, event); | ||
} | ||
var onClickCb = useCallback(function () { | ||
if (noClick) { | ||
return; | ||
} // In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout() | ||
// to ensure React can handle state changes | ||
// See: https://github.com/react-dropzone/react-dropzone/issues/450 | ||
if (fileRejections.length > 0 && onDropRejected) { | ||
onDropRejected(fileRejections, event); | ||
} | ||
if (acceptedFiles.length > 0 && onDropAccepted) { | ||
onDropAccepted(acceptedFiles, event); | ||
} | ||
}); | ||
if (isIeOrEdge()) { | ||
setTimeout(openFileDialog, 0); | ||
} else { | ||
openFileDialog(); | ||
} | ||
}, [inputRef, noClick, openFileDialog]); | ||
dispatch({ | ||
type: 'reset' | ||
}); | ||
}, [multiple, accept, minSize, maxSize, maxFiles, getFilesFromEvent, onDrop, onDropAccepted, onDropRejected, noDragEventsBubbling, validator]); | ||
var composeHandler = function composeHandler(fn) { | ||
@@ -838,3 +868,3 @@ return disabled ? null : fn; | ||
case 'openDialog': | ||
return _objectSpread(_objectSpread({}, state), {}, { | ||
return _objectSpread(_objectSpread({}, initialState), {}, { | ||
isFileDialogActive: true | ||
@@ -871,2 +901,4 @@ }); | ||
function noop() {} | ||
export { ErrorCode } from './utils'; |
@@ -1,3 +0,9 @@ | ||
function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } | ||
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } | ||
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function _typeof(obj) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (obj) { return typeof obj; } : function (obj) { return obj && "function" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }, _typeof(obj); } | ||
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } | ||
@@ -169,2 +175,32 @@ | ||
}; | ||
} | ||
/** | ||
* canUseFileSystemAccessAPI checks if the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) | ||
* is supported by the browser. | ||
* @returns {boolean} | ||
*/ | ||
export function canUseFileSystemAccessAPI() { | ||
return 'showOpenFilePicker' in window; | ||
} | ||
/** | ||
* filePickerOptionsTypes returns the {types} option for https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker | ||
* based on the accept attr (see https://github.com/react-dropzone/attr-accept) | ||
* E.g: converts ['image/*', 'text/*'] to {'image/*': [], 'text/*': []} | ||
* @param {string|string[]} accept | ||
*/ | ||
export function filePickerOptionsTypes(accept) { | ||
accept = typeof accept === 'string' ? accept.split(',') : accept; | ||
return [{ | ||
description: 'everything', | ||
// TODO: Need to handle filtering more elegantly than this! | ||
accept: Array.isArray(accept) // Accept just MIME types as per spec | ||
// NOTE: accept can be https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers | ||
? accept.filter(function (item) { | ||
return item === 'audio/*' || item === 'video/*' || item === 'image/*' || item === 'text/*' || /\w+\/[-+.\w]+/g.test(item); | ||
}).reduce(function (a, b) { | ||
return _objectSpread(_objectSpread({}, a), {}, _defineProperty({}, b, [])); | ||
}, {}) : {} | ||
}]; | ||
} |
@@ -89,3 +89,3 @@ If you'd like to prevent drag events propagation from the child to parent, you can use the `{noDragEventsBubbling}` property on the child: | ||
Note that you can prevent the default behavior for click and keyboard events if you omit the input: | ||
Or you can prevent the default behavior for both click and keyboard events if you omit the input: | ||
```jsx harmony | ||
@@ -115,2 +115,4 @@ import React from 'react'; | ||
**NOTE** If the browser supports the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API), removing the `<input>` has no effect. | ||
If you'd like to selectively turn off the default dropzone behavior for drag events, use the `{noDrag}` property: | ||
@@ -117,0 +119,0 @@ ```jsx harmony |
153
package.json
@@ -8,5 +8,5 @@ { | ||
"scripts": { | ||
"ci": "git-cz", | ||
"cz": "git-cz", | ||
"clean": "rimraf ./dist", | ||
"build": "npm run clean && npm run build:umd && npm run build:es", | ||
"build": "yarn clean && yarn build:umd && yarn build:es", | ||
"build:umd": "cross-env NODE_ENV=es rollup -c", | ||
@@ -16,8 +16,10 @@ "build:es": "cross-env BABEL_ENV=es babel ./src --out-dir ./dist/es --ignore '**/*.spec.js'", | ||
"styleguide": "styleguidist build", | ||
"test": "cross-env NODE_ENV=test npm run eslint:src && jest --coverage && npm run typescript", | ||
"test": "cross-env NODE_ENV=test yarn eslint:src && jest --coverage && yarn typescript", | ||
"test:watch": "cross-env NODE_ENV=test jest --watch", | ||
"eslint:src": "eslint .", | ||
"commitmsg": "commitlint -e", | ||
"precommit": "lint-staged", | ||
"prepublish": "npm run build && npm run size", | ||
"prepublish": "yarn build && yarn size", | ||
"_postinstall": "husky install", | ||
"prepublishOnly": "pinst --disable", | ||
"postpublish": "pinst --enable", | ||
"logo": "cd logo && sketchtool export artboards logo.sketch", | ||
@@ -41,14 +43,18 @@ "imagemin": "imagemin --out-dir=logo --plugin=pngquant --plugin=svgo", | ||
"*.js": [ | ||
"eslint --fix", | ||
"git add" | ||
"eslint --fix" | ||
], | ||
"*.ts": [ | ||
"tslint" | ||
"eslint ." | ||
], | ||
"*.{svg,png}": [ | ||
"imagemin", | ||
"git add" | ||
"imagemin" | ||
] | ||
}, | ||
"config": { | ||
"commitizen": { | ||
"path": "@commitlint/prompt" | ||
} | ||
}, | ||
"jest": { | ||
"testEnvironment": "jsdom", | ||
"clearMocks": true, | ||
@@ -97,81 +103,78 @@ "setupFilesAfterEnv": [ | ||
"dependencies": { | ||
"attr-accept": "^2.2.1", | ||
"file-selector": "^0.2.2", | ||
"prop-types": "^15.7.2" | ||
"attr-accept": "^2.2.2", | ||
"file-selector": "^0.4.0", | ||
"prop-types": "^15.8.1" | ||
}, | ||
"devDependencies": { | ||
"@babel/cli": "^7.11.6", | ||
"@babel/core": "^7.11.6", | ||
"@babel/plugin-external-helpers": "^7.10.4", | ||
"@babel/plugin-proposal-do-expressions": "^7.10.4", | ||
"@babel/plugin-proposal-export-default-from": "^7.10.4", | ||
"@babel/plugin-proposal-logical-assignment-operators": "^7.11.0", | ||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4", | ||
"@babel/plugin-proposal-optional-chaining": "^7.11.0", | ||
"@babel/plugin-proposal-pipeline-operator": "^7.10.5", | ||
"@babel/plugin-transform-runtime": "^7.11.5", | ||
"@babel/preset-env": "^7.11.5", | ||
"@babel/preset-react": "^7.10.4", | ||
"@babel/register": "^7.11.5", | ||
"@commitlint/cli": "^9.1.2", | ||
"@commitlint/config-angular": "^9.1.2", | ||
"@commitlint/prompt": "^9.1.2", | ||
"@commitlint/prompt-cli": "^9.1.2", | ||
"@size-limit/preset-small-lib": "^4.5.7", | ||
"@testing-library/dom": "^8.0.0", | ||
"@testing-library/jest-dom": "^5.11.4", | ||
"@testing-library/react": "^12.0.0", | ||
"@testing-library/react-hooks": "^7.0.0", | ||
"@babel/cli": "^7.16.8", | ||
"@babel/core": "^7.16.12", | ||
"@babel/eslint-parser": "^7.16.5", | ||
"@babel/plugin-external-helpers": "^7.16.7", | ||
"@babel/plugin-proposal-do-expressions": "^7.16.7", | ||
"@babel/plugin-proposal-export-default-from": "^7.16.7", | ||
"@babel/plugin-proposal-logical-assignment-operators": "^7.16.7", | ||
"@babel/plugin-proposal-nullish-coalescing-operator": "^7.16.7", | ||
"@babel/plugin-proposal-optional-chaining": "^7.16.7", | ||
"@babel/plugin-proposal-pipeline-operator": "^7.16.7", | ||
"@babel/plugin-transform-runtime": "^7.16.10", | ||
"@babel/preset-env": "^7.16.11", | ||
"@babel/preset-react": "^7.16.7", | ||
"@babel/register": "^7.16.9", | ||
"@commitlint/cli": "^16.1.0", | ||
"@commitlint/config-angular": "^16.0.0", | ||
"@commitlint/prompt": "^16.1.0", | ||
"@commitlint/prompt-cli": "^16.1.0", | ||
"@rollup/plugin-babel": "^5.3.0", | ||
"@rollup/plugin-commonjs": "^21.0.1", | ||
"@rollup/plugin-node-resolve": "^13.1.3", | ||
"@size-limit/preset-small-lib": "^7.0.5", | ||
"@size-limit/webpack": "^7.0.5", | ||
"@size-limit/webpack-why": "^7.0.5", | ||
"@testing-library/dom": "^8.11.3", | ||
"@testing-library/jest-dom": "^5.16.1", | ||
"@testing-library/react": "^12.1.2", | ||
"@testing-library/react-hooks": "^7.0.2", | ||
"@types/react": "^17.0.0", | ||
"@types/react-dom": "^17.0.0", | ||
"babel-eslint": "10.x", | ||
"babel-jest": "^26.3.0", | ||
"babel-plugin-add-module-exports": "^1.0.2", | ||
"@typescript-eslint/eslint-plugin": "^5.10.1", | ||
"@typescript-eslint/parser": "^5.10.1", | ||
"babel-jest": "^27.4.6", | ||
"babel-plugin-add-module-exports": "^1.0.4", | ||
"babel-plugin-dynamic-import-node": "^2.3.3", | ||
"commitizen": "^4.2.1", | ||
"cross-env": "^7.0.2", | ||
"eslint": "7.x", | ||
"eslint-config-okonet": "^7.0.2", | ||
"eslint-config-prettier": "6.x", | ||
"eslint-plugin-import": "2.x", | ||
"eslint-plugin-jsx-a11y": "6.x", | ||
"eslint-plugin-node": "11.x", | ||
"eslint-plugin-prettier": "3.x", | ||
"eslint-plugin-react": "7.x", | ||
"eslint-plugin-react-hooks": "4.x", | ||
"husky": "^4.2.5", | ||
"imagemin-cli": "^6.0.0", | ||
"imagemin-pngquant": "^9.0.0", | ||
"jest": "^26.4.2", | ||
"lint-staged": "^10.3.0", | ||
"markdownlint-cli": "^0.23.2", | ||
"prettier": "*", | ||
"commitizen": "^4.2.4", | ||
"cross-env": "^7.0.3", | ||
"eslint": "^8.8.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.25.4", | ||
"eslint-plugin-jsx-a11y": "^6.5.1", | ||
"eslint-plugin-node": "^11.1.0", | ||
"eslint-plugin-prettier": "^4.0.0", | ||
"eslint-plugin-react": "^7.28.0", | ||
"eslint-plugin-react-hooks": "^4.3.0", | ||
"husky": "^7.0.4", | ||
"imagemin-cli": "^7.0.0", | ||
"imagemin-pngquant": "^9.0.2", | ||
"jest": "^27.4.7", | ||
"lint-staged": "^12.3.2", | ||
"markdownlint-cli": "^0.30.0", | ||
"pinst": "^2.1.6", | ||
"prettier": "^2.5.1", | ||
"react": "^17.0.0", | ||
"react-dom": "^17.0.0", | ||
"react-styleguidist": "^11.0.10", | ||
"react-styleguidist": "^11.2.0", | ||
"react-test-renderer": "^17.0.0", | ||
"rimraf": "^3.0.2", | ||
"rollup": "^2.26.10", | ||
"rollup-plugin-babel": "^4.4.0", | ||
"rollup-plugin-commonjs": "^10.1.0", | ||
"rollup-plugin-node-resolve": "^5.2.0", | ||
"rollup-plugin-uglify": "^6.0.4", | ||
"sinon": "^9.0.3", | ||
"size-limit": "^4.5.7", | ||
"style-loader": "^1.2.1", | ||
"styled-components": "^5.2.0", | ||
"tslint": "^6.1.3", | ||
"rollup": "^2.66.1", | ||
"rollup-plugin-terser": "^7.0.2", | ||
"size-limit": "^7.0.5", | ||
"style-loader": "^3.3.1", | ||
"styled-components": "^5.3.3", | ||
"typescript": "^4.0.2", | ||
"webpack": "^4.44.1", | ||
"webpack": "^5.67.0", | ||
"webpack-blocks": "^2.1.0" | ||
}, | ||
"typings": "typings/react-dropzone.d.ts", | ||
"config": { | ||
"commitizen": { | ||
"path": "@commitlint/prompt" | ||
} | ||
}, | ||
"version": "11.5.3", | ||
"version": "11.6.0", | ||
"engines": { | ||
"node": ">= 10" | ||
"node": ">= 10.13" | ||
}, | ||
@@ -178,0 +181,0 @@ "browserslist": [ |
106
README.md
![react-dropzone logo](https://raw.githubusercontent.com/react-dropzone/react-dropzone/master/logo/logo.png) | ||
# react-dropzone | ||
[![npm](https://img.shields.io/npm/v/react-dropzone.svg?style=flat-square)](https://www.npmjs.com/package/react-dropzone) | ||
[![GitHub Workflow Status](https://img.shields.io/github/workflow/status/react-dropzone/react-dropzone/Test?label=tests&style=flat-square)](https://github.com/react-dropzone/react-dropzone/actions?query=workflow%3ATest) | ||
[![Tests](https://img.shields.io/github/workflow/status/react-dropzone/react-dropzone/Test?label=tests&style=flat-square)](https://github.com/react-dropzone/react-dropzone/actions?query=workflow%3ATest) | ||
[![codecov](https://img.shields.io/codecov/c/gh/react-dropzone/react-dropzone/master.svg?style=flat-square)](https://codecov.io/gh/react-dropzone/react-dropzone) | ||
[![Open Collective](https://img.shields.io/opencollective/backers/react-dropzone.svg?style=flat-square)](#backers) | ||
[![Open Collective](https://img.shields.io/opencollective/sponsors/react-dropzone.svg?style=flat-square)](#sponsors) | ||
[![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/react-dropzone/react-dropzone) | ||
[![Open Collective Backers](https://img.shields.io/opencollective/backers/react-dropzone.svg?style=flat-square)](#backers) | ||
[![Open Collective Sponsors](https://img.shields.io/opencollective/sponsors/react-dropzone.svg?style=flat-square)](#sponsors) | ||
[![Gitpod](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/react-dropzone/react-dropzone) | ||
@@ -18,3 +17,2 @@ Simple React hook to create a HTML5-compliant drag'n'drop zone for files. | ||
## Installation | ||
Install it from npm and include it in your React build process (using [Webpack](http://webpack.github.io/), [Browserify](http://browserify.org/), etc). | ||
@@ -57,4 +55,2 @@ | ||
**IMPORTANT**: Under the hood, this lib makes use of [hooks](https://reactjs.org/docs/hooks-intro.html), therefore, using it requires React `>= 16.8`. | ||
Or the wrapper component for the hook: | ||
@@ -77,7 +73,4 @@ ```jsx static | ||
If you want to access file contents you have to use the [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader): | ||
**Warning**: On most recent browsers versions, the files given by `onDrop` won't have properties `path` or `fullPath`, see [this SO question](https://stackoverflow.com/a/23005925/2275818) and [this issue](https://github.com/react-dropzone/react-dropzone/issues/477). | ||
Furthermore, if you want to access file contents you have to use the [FileReader API](https://developer.mozilla.org/en-US/docs/Web/API/FileReader): | ||
```jsx static | ||
@@ -116,3 +109,2 @@ import React, {useCallback} from 'react' | ||
## Dropzone Props Getters | ||
The dropzone property getters are just two functions that return objects with properties which you need to use to create the drag 'n' drop zone. | ||
@@ -141,7 +133,9 @@ The root properties can be applied to whatever element you want, whereas the input properties must be applied to an `<input>`: | ||
{...getRootProps({ | ||
onClick: event => console.log(event) | ||
onClick: event => console.log(event), | ||
role: 'button', | ||
'aria-label': 'drag and drop area', | ||
... | ||
})} | ||
/> | ||
``` | ||
> ♿ this is also where you pass accessibility props like `role`, `aria-labelledby` ...etc. | ||
@@ -154,6 +148,5 @@ In the example above, the provided `{onClick}` handler will be invoked before the internal one, therefore, internal callbacks can be prevented by simply using [stopPropagation](https://developer.mozilla.org/en-US/docs/Web/API/Event/stopPropagation). | ||
## Refs | ||
Both `getRootProps` and `getInputProps` accept a custom `refKey` (defaults to `ref`) as one of the attributes passed down in the parameter. | ||
This can be useful when the element you're trying to apply the props from either one of those fns does not expose a reference to the element, e.g.: | ||
This can be useful when the element you're trying to apply the props from either one of those fns does not expose a reference to the element, e.g: | ||
@@ -179,3 +172,3 @@ ```jsx static | ||
If you're working with [Material UI](https://material-ui.com) and would like to apply the root props on some component that does not expose a ref, use [RootRef](https://material-ui.com/api/root-ref): | ||
If you're working with [Material UI v4](https://v4.mui.com/) and would like to apply the root props on some component that does not expose a ref, use [RootRef](https://v4.mui.com/api/root-ref/): | ||
@@ -200,3 +193,3 @@ ```jsx static | ||
*Important*: do not set the `ref` prop on the elements where `getRootProps()`/`getInputProps()` props are set, instead, get the refs from the hook itself: | ||
**IMPORTANT**: do not set the `ref` prop on the elements where `getRootProps()`/`getInputProps()` props are set, instead, get the refs from the hook itself: | ||
@@ -243,8 +236,7 @@ ```jsx static | ||
## Testing | ||
*Important*: `react-dropzone` makes some of its drag 'n' drop callbacks asynchronous to enable promise based `getFilesFromEvent()` functions. In order to test components that use this library, you may want to use the [react-testing-library](https://github.com/testing-library/react-testing-library): | ||
`react-dropzone` makes some of its drag 'n' drop callbacks asynchronous to enable promise based `getFilesFromEvent()` functions. In order to test components that use this library, you need to use the [react-testing-library](https://github.com/testing-library/react-testing-library): | ||
```js static | ||
import React from 'react' | ||
import Dropzone from 'react-dropzone' | ||
import { act, fireEvent, render, waitFor } from '@testing-library/react' | ||
import {act, fireEvent, render, waitFor} from '@testing-library/react' | ||
@@ -301,10 +293,70 @@ test('invoke onDragEnter when dragenter event occurs', async () => { | ||
*Note*: using [Enzyme](https://airbnb.io/enzyme) for testing is not supported at the moment, see [#2011](https://github.com/airbnb/enzyme/issues/2011). | ||
**NOTE**: using [Enzyme](https://airbnb.io/enzyme) for testing is not supported at the moment, see [#2011](https://github.com/airbnb/enzyme/issues/2011). | ||
More examples for this can be found in `react-dropzone`s own [test suites](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.spec.js). | ||
More examples for this can be found in `react-dropzone`'s own [test suites](https://github.com/react-dropzone/react-dropzone/blob/master/src/index.spec.js). | ||
## Need image editing? | ||
## Caveats | ||
### Required React Version | ||
React [16.8](https://reactjs.org/blog/2019/02/06/react-v16.8.0.html) or above is required because we use [hooks](https://reactjs.org/docs/hooks-intro.html) (the lib itself is a hook). | ||
### File Paths | ||
Files returned by the hook or passed as arg to the `onDrop` cb won't have the properties `path` or `fullPath`. | ||
For more inf check [this SO question](https://stackoverflow.com/a/23005925/2275818) and [this issue](https://github.com/react-dropzone/react-dropzone/issues/477). | ||
### Not a File Uploader | ||
This lib is not a file uploader; as such, it does not process files or provide any way to make HTTP requests to some server; if you're looking for that, checkout [filepond](https://pqina.nl/filepond) or [uppy.io](https://uppy.io/). | ||
### Using \<label\> as Root | ||
If you use [\<label\>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/label) as the root element, the file dialog will be opened twice; see [#1107](https://github.com/react-dropzone/react-dropzone/issues/1107) why. To avoid this, use `noClick`: | ||
```jsx static | ||
import React, {useCallback} from 'react' | ||
import {useDropzone} from 'react-dropzone' | ||
function MyDropzone() { | ||
const {getRootProps, getInputProps} = useDropzone({noClick: true}) | ||
return ( | ||
<label {...getRootProps()}> | ||
<input {...getInputProps()} /> | ||
</label> | ||
) | ||
} | ||
``` | ||
### Using open() on Click | ||
If you bind a click event on an inner element and use `open()`, it will trigger a click on the root element too, resulting in the file dialog opening twice. To prevent this, use the `noClick` on the root: | ||
```jsx static | ||
import React, {useCallback} from 'react' | ||
import {useDropzone} from 'react-dropzone' | ||
function MyDropzone() { | ||
const {getRootProps, getInputProps, open} = useDropzone({noClick: true}) | ||
return ( | ||
<div {...getRootProps()}> | ||
<input {...getInputProps()} /> | ||
<button type="button" onClick={open}> | ||
Open | ||
</button> | ||
</div> | ||
) | ||
} | ||
``` | ||
### File Dialog Cancel Callback | ||
The `onFileDialogCancel()` cb is unstable in most browsers, meaning, there's a good chance of it being triggered even though you have selected files. | ||
We rely on using a timeout of `300ms` after the window is focused (the window `onfocus` event is triggered when the file select dialog is closed) to check if any files were selected and trigger `onFileDialogCancel` if none were selected. | ||
As one can imagine, this doesn't really work if there's a lot of files or large files as by the time we trigger the check, the browser is still processing the files and no `onchange` events are triggered yet on the input. Check [#1031](https://github.com/react-dropzone/react-dropzone/issues/1031) for more info. | ||
Fortunately, there's the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API), which is currently a working draft and some browsers support it (see [browser compatibility](https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker#browser_compatibility)), that provides a reliable way to prompt the user for file selection and capture cancellation. | ||
And this lib makes use of it if available. Though, there's a small catch: using file extensions for the `accept` property is not supported; you must use MIME types as described in [common MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types). Also check [accepting specific file types](https://react-dropzone.js.org/#section-accepting-specific-file-types) for more info on the subject of `accept` limitations. | ||
## Supported Browsers | ||
We use [browserslist](https://github.com/browserslist/browserslist) config to state the browser support for this lib, so check it out on [browserslist.dev](https://browserslist.dev/?q=ZGVmYXVsdHM%3D). | ||
## Need image editing? | ||
React Dropzone integrates perfectly with [Pintura Image Editor](https://pqina.nl/pintura/?ref=react-dropzone), creating a modern image editing experience. Pintura supports crop aspect ratios, resizing, rotating, cropping, annotating, filtering, and much more. | ||
@@ -314,6 +366,3 @@ | ||
## Supported Browsers | ||
We use [browserslist](https://github.com/browserslist/browserslist) config to state the browser support for this lib, so check it out on [browserslist.dev](https://browserslist.dev/?q=ZGVmYXVsdHM%3D). | ||
## Support | ||
@@ -392,3 +441,2 @@ | ||
## License | ||
MIT |
@@ -1,5 +0,5 @@ | ||
const nodeResolve = require('rollup-plugin-node-resolve') | ||
const commonjs = require('rollup-plugin-commonjs') | ||
const babel = require('rollup-plugin-babel') | ||
const { uglify } = require('rollup-plugin-uglify') | ||
const { nodeResolve } = require('@rollup/plugin-node-resolve') | ||
const commonjs = require('@rollup/plugin-commonjs') | ||
const { babel } = require('@rollup/plugin-babel') | ||
const { terser } = require('rollup-plugin-terser') | ||
@@ -26,6 +26,9 @@ const umdGlobals = { | ||
commonjs({ include: '**/node_modules/**' }), | ||
babel({ exclude: '**/node_modules/**' }), | ||
uglify() | ||
babel({ | ||
exclude: '**/node_modules/**', | ||
babelHelpers: 'bundled', | ||
}), | ||
terser() | ||
] | ||
} | ||
] |
305
src/index.js
@@ -13,3 +13,3 @@ /* eslint prefer-template: 0 */ | ||
import PropTypes from 'prop-types' | ||
import { fromEvent } from 'file-selector' | ||
import {fromEvent} from 'file-selector' | ||
import { | ||
@@ -20,2 +20,4 @@ allFilesAccepted, | ||
fileMatchSize, | ||
filePickerOptionsTypes, | ||
canUseFileSystemAccessAPI, | ||
isEvtWithFiles, | ||
@@ -42,9 +44,9 @@ isIeOrEdge, | ||
*/ | ||
const Dropzone = forwardRef(({ children, ...params }, ref) => { | ||
const { open, ...props } = useDropzone(params) | ||
const Dropzone = forwardRef(({children, ...params}, ref) => { | ||
const {open, ...props} = useDropzone(params) | ||
useImperativeHandle(ref, () => ({ open }), [open]) | ||
useImperativeHandle(ref, () => ({open}), [open]) | ||
// TODO: Figure out why react-styleguidist cannot create docs if we don't return a jsx element | ||
return <Fragment>{children({ ...props, open })}</Fragment> | ||
return <Fragment>{children({...props, open})}</Fragment> | ||
}) | ||
@@ -164,5 +166,5 @@ | ||
/** | ||
* Cb for when opening the file dialog | ||
*/ | ||
/** | ||
* Cb for when opening the file dialog | ||
*/ | ||
onFileDialogOpen: PropTypes.func, | ||
@@ -427,2 +429,9 @@ | ||
const onFileDialogOpenCb = useMemo( | ||
() => typeof onFileDialogOpen === 'function' ? onFileDialogOpen : noop, | ||
[onFileDialogOpen]) | ||
const onFileDialogCancelCb = useMemo( | ||
() => typeof onFileDialogCancel === 'function' ? onFileDialogCancel : noop, | ||
[onFileDialogCancel]) | ||
const rootRef = useRef(null) | ||
@@ -432,16 +441,4 @@ const inputRef = useRef(null) | ||
const [state, dispatch] = useReducer(reducer, initialState) | ||
const { isFocused, isFileDialogActive, draggedFiles } = state | ||
const {isFocused, isFileDialogActive, draggedFiles} = state | ||
// Fn for opening the file dialog programmatically | ||
const openFileDialog = useCallback(() => { | ||
if (inputRef.current) { | ||
dispatch({ type: 'openDialog' }) | ||
if (typeof onFileDialogOpen === 'function') { | ||
onFileDialogOpen() | ||
} | ||
inputRef.current.value = null | ||
inputRef.current.click() | ||
} | ||
}, [dispatch, onFileDialogOpen]) | ||
// Update file dialog active state when the window is focused on | ||
@@ -453,10 +450,7 @@ const onWindowFocus = () => { | ||
if (inputRef.current) { | ||
const { files } = inputRef.current | ||
const {files} = inputRef.current | ||
if (!files.length) { | ||
dispatch({ type: 'closeDialog' }) | ||
if (typeof onFileDialogCancel === 'function') { | ||
onFileDialogCancel() | ||
} | ||
dispatch({type: 'closeDialog'}) | ||
onFileDialogCancelCb() | ||
} | ||
@@ -468,2 +462,6 @@ } | ||
useEffect(() => { | ||
if (canUseFileSystemAccessAPI()) { | ||
return () => {} | ||
} | ||
window.addEventListener('focus', onWindowFocus, false) | ||
@@ -473,44 +471,4 @@ return () => { | ||
} | ||
}, [inputRef, isFileDialogActive, onFileDialogCancel]) | ||
}, [inputRef, isFileDialogActive, onFileDialogCancelCb]) | ||
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone | ||
const onKeyDownCb = useCallback( | ||
event => { | ||
// Ignore keyboard events bubbling up the DOM tree | ||
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { | ||
return | ||
} | ||
if (event.keyCode === 32 || event.keyCode === 13) { | ||
event.preventDefault() | ||
openFileDialog() | ||
} | ||
}, | ||
[rootRef, inputRef, openFileDialog] | ||
) | ||
// Update focus state for the dropzone | ||
const onFocusCb = useCallback(() => { | ||
dispatch({ type: 'focus' }) | ||
}, []) | ||
const onBlurCb = useCallback(() => { | ||
dispatch({ type: 'blur' }) | ||
}, []) | ||
// Cb to open the file dialog when click occurs on the dropzone | ||
const onClickCb = useCallback(() => { | ||
if (noClick) { | ||
return | ||
} | ||
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout() | ||
// to ensure React can handle state changes | ||
// See: https://github.com/react-dropzone/react-dropzone/issues/450 | ||
if (isIeOrEdge()) { | ||
setTimeout(openFileDialog, 0) | ||
} else { | ||
openFileDialog() | ||
} | ||
}, [inputRef, noClick, openFileDialog]) | ||
const dragTargetsRef = useRef([]) | ||
@@ -626,2 +584,62 @@ const onDocumentDrop = event => { | ||
const setFiles = useCallback((files, event) => { | ||
const acceptedFiles = [] | ||
const fileRejections = [] | ||
files.forEach(file => { | ||
const [accepted, acceptError] = fileAccepted(file, accept) | ||
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize) | ||
const customErrors = validator ? validator(file) : null; | ||
if (accepted && sizeMatch && !customErrors) { | ||
acceptedFiles.push(file) | ||
} else { | ||
let errors = [acceptError, sizeError]; | ||
if (customErrors) { | ||
errors = errors.concat(customErrors); | ||
} | ||
fileRejections.push({file, errors: errors.filter(e => e)}) | ||
} | ||
}) | ||
if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) { | ||
// Reject everything and empty accepted files | ||
acceptedFiles.forEach(file => { | ||
fileRejections.push({file, errors: [TOO_MANY_FILES_REJECTION]}) | ||
}) | ||
acceptedFiles.splice(0) | ||
} | ||
dispatch({ | ||
acceptedFiles, | ||
fileRejections, | ||
type: 'setFiles' | ||
}) | ||
if (onDrop) { | ||
onDrop(acceptedFiles, fileRejections, event) | ||
} | ||
if (fileRejections.length > 0 && onDropRejected) { | ||
onDropRejected(fileRejections, event) | ||
} | ||
if (acceptedFiles.length > 0 && onDropAccepted) { | ||
onDropAccepted(acceptedFiles, event) | ||
} | ||
}, [ | ||
dispatch, | ||
multiple, | ||
accept, | ||
minSize, | ||
maxSize, | ||
maxFiles, | ||
onDrop, | ||
onDropAccepted, | ||
onDropRejected, | ||
validator | ||
]); | ||
const onDropCb = useCallback( | ||
@@ -641,68 +659,87 @@ event => { | ||
} | ||
setFiles(files, event) | ||
}) | ||
} | ||
dispatch({type: 'reset'}) | ||
}, | ||
[ | ||
getFilesFromEvent, | ||
setFiles, | ||
noDragEventsBubbling | ||
] | ||
) | ||
const acceptedFiles = [] | ||
const fileRejections = [] | ||
// Fn for opening the file dialog programmatically | ||
const openFileDialog = useCallback(() => { | ||
if (canUseFileSystemAccessAPI()) { | ||
dispatch({type: 'openDialog'}) | ||
onFileDialogOpenCb() | ||
// https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker | ||
const opts = { | ||
multiple, | ||
types: filePickerOptionsTypes(accept) | ||
}; | ||
window.showOpenFilePicker(opts) | ||
.then(handles => getFilesFromEvent(handles)) | ||
.then(files => setFiles(files, null)) | ||
.catch(e => onFileDialogCancelCb(e)) | ||
.finally(() => dispatch({type: 'closeDialog'})); | ||
return | ||
} | ||
files.forEach(file => { | ||
const [accepted, acceptError] = fileAccepted(file, accept) | ||
const [sizeMatch, sizeError] = fileMatchSize(file, minSize, maxSize) | ||
const customErrors = validator ? validator(file) : null; | ||
if (inputRef.current) { | ||
dispatch({type: 'openDialog'}) | ||
onFileDialogOpenCb() | ||
inputRef.current.value = null | ||
inputRef.current.click() | ||
} | ||
}, [ | ||
dispatch, | ||
onFileDialogOpenCb, | ||
onFileDialogCancelCb, | ||
setFiles, | ||
accept, | ||
multiple | ||
]) | ||
if (accepted && sizeMatch && !customErrors) { | ||
acceptedFiles.push(file) | ||
} else { | ||
let errors = [acceptError, sizeError]; | ||
// Cb to open the file dialog when SPACE/ENTER occurs on the dropzone | ||
const onKeyDownCb = useCallback( | ||
event => { | ||
// Ignore keyboard events bubbling up the DOM tree | ||
if (!rootRef.current || !rootRef.current.isEqualNode(event.target)) { | ||
return | ||
} | ||
if (customErrors) { | ||
errors = errors.concat(customErrors); | ||
} | ||
if (event.keyCode === 32 || event.keyCode === 13) { | ||
event.preventDefault() | ||
openFileDialog() | ||
} | ||
}, | ||
[rootRef, inputRef, openFileDialog] | ||
) | ||
fileRejections.push({ file, errors: errors.filter(e => e) }) | ||
} | ||
}) | ||
// Update focus state for the dropzone | ||
const onFocusCb = useCallback(() => { | ||
dispatch({type: 'focus'}) | ||
}, []) | ||
const onBlurCb = useCallback(() => { | ||
dispatch({type: 'blur'}) | ||
}, []) | ||
if ((!multiple && acceptedFiles.length > 1) || (multiple && maxFiles >= 1 && acceptedFiles.length > maxFiles)) { | ||
// Reject everything and empty accepted files | ||
acceptedFiles.forEach(file => { | ||
fileRejections.push({ file, errors: [TOO_MANY_FILES_REJECTION] }) | ||
}) | ||
acceptedFiles.splice(0) | ||
} | ||
// Cb to open the file dialog when click occurs on the dropzone | ||
const onClickCb = useCallback(() => { | ||
if (noClick) { | ||
return | ||
} | ||
dispatch({ | ||
acceptedFiles, | ||
fileRejections, | ||
type: 'setFiles' | ||
}) | ||
// In IE11/Edge the file-browser dialog is blocking, therefore, use setTimeout() | ||
// to ensure React can handle state changes | ||
// See: https://github.com/react-dropzone/react-dropzone/issues/450 | ||
if (isIeOrEdge()) { | ||
setTimeout(openFileDialog, 0) | ||
} else { | ||
openFileDialog() | ||
} | ||
}, [inputRef, noClick, openFileDialog]) | ||
if (onDrop) { | ||
onDrop(acceptedFiles, fileRejections, event) | ||
} | ||
if (fileRejections.length > 0 && onDropRejected) { | ||
onDropRejected(fileRejections, event) | ||
} | ||
if (acceptedFiles.length > 0 && onDropAccepted) { | ||
onDropAccepted(acceptedFiles, event) | ||
} | ||
}) | ||
} | ||
dispatch({ type: 'reset' }) | ||
}, | ||
[ | ||
multiple, | ||
accept, | ||
minSize, | ||
maxSize, | ||
maxFiles, | ||
getFilesFromEvent, | ||
onDrop, | ||
onDropAccepted, | ||
onDropRejected, | ||
noDragEventsBubbling, | ||
validator | ||
] | ||
) | ||
const composeHandler = fn => { | ||
@@ -748,3 +785,3 @@ return disabled ? null : fn | ||
[refKey]: rootRef, | ||
...(!disabled && !noKeyboard ? { tabIndex: 0 } : {}), | ||
...(!disabled && !noKeyboard ? {tabIndex: 0} : {}), | ||
...rest | ||
@@ -773,3 +810,3 @@ }), | ||
const getInputProps = useMemo( | ||
() => ({ refKey = 'ref', onChange, onClick, ...rest } = {}) => { | ||
() => ({refKey = 'ref', onChange, onClick, ...rest} = {}) => { | ||
const inputProps = { | ||
@@ -779,3 +816,3 @@ accept, | ||
type: 'file', | ||
style: { display: 'none' }, | ||
style: {display: 'none'}, | ||
onChange: composeHandler(composeEventHandlers(onChange, onDropCb)), | ||
@@ -797,3 +834,3 @@ onClick: composeHandler(composeEventHandlers(onClick, onInputElementClick)), | ||
const fileCount = draggedFiles.length | ||
const isDragAccept = fileCount > 0 && allFilesAccepted({ files: draggedFiles, accept, minSize, maxSize, multiple, maxFiles }) | ||
const isDragAccept = fileCount > 0 && allFilesAccepted({files: draggedFiles, accept, minSize, maxSize, multiple, maxFiles}) | ||
const isDragReject = fileCount > 0 && !isDragAccept | ||
@@ -829,3 +866,3 @@ | ||
return { | ||
...state, | ||
...initialState, | ||
isFileDialogActive: true | ||
@@ -840,3 +877,3 @@ } | ||
/* eslint no-case-declarations: 0 */ | ||
const { isDragActive, draggedFiles } = action | ||
const {isDragActive, draggedFiles} = action | ||
return { | ||
@@ -862,2 +899,4 @@ ...state, | ||
export { ErrorCode } from './utils' | ||
function noop() {} | ||
export {ErrorCode} from './utils' |
@@ -69,4 +69,4 @@ import accepts from 'attr-accept' | ||
export function allFilesAccepted({ files, accept, minSize, maxSize, multiple, maxFiles }) { | ||
if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles) ) { | ||
export function allFilesAccepted({files, accept, minSize, maxSize, multiple, maxFiles}) { | ||
if ((!multiple && files.length > 1) || (multiple && maxFiles >= 1 && files.length > maxFiles)) { | ||
return false; | ||
@@ -146,1 +146,35 @@ } | ||
} | ||
/** | ||
* canUseFileSystemAccessAPI checks if the [File System Access API](https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API) | ||
* is supported by the browser. | ||
* @returns {boolean} | ||
*/ | ||
export function canUseFileSystemAccessAPI() { | ||
return 'showOpenFilePicker' in window; | ||
} | ||
/** | ||
* filePickerOptionsTypes returns the {types} option for https://developer.mozilla.org/en-US/docs/Web/API/window/showOpenFilePicker | ||
* based on the accept attr (see https://github.com/react-dropzone/attr-accept) | ||
* E.g: converts ['image/*', 'text/*'] to {'image/*': [], 'text/*': []} | ||
* @param {string|string[]} accept | ||
*/ | ||
export function filePickerOptionsTypes(accept) { | ||
accept = typeof accept === 'string' ? accept.split(',') : accept | ||
return [{ | ||
description: 'everything', | ||
// TODO: Need to handle filtering more elegantly than this! | ||
accept: Array.isArray(accept) | ||
// Accept just MIME types as per spec | ||
// NOTE: accept can be https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#unique_file_type_specifiers | ||
? accept.filter(item => | ||
item === 'audio/*' || | ||
item === 'video/*' || | ||
item === 'image/*' || | ||
item === 'text/*' || | ||
/\w+\/[-+.\w]+/g.test(item) | ||
).reduce((a, b) => ({...a, [b]: []}), {}) | ||
: {}, | ||
}]; | ||
} |
@@ -5,8 +5,6 @@ beforeEach(() => { | ||
describe('fileMatchSize()', () => { | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -46,5 +44,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -94,5 +91,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -115,5 +111,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -152,5 +147,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -201,5 +195,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -241,5 +234,4 @@ | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -252,4 +244,4 @@ it('rejects file when multiple accept criteria', () => { | ||
expect(utils.allFilesAccepted({ files, multiple: false, maxFiles: 10 })).toEqual(false) | ||
expect(utils.allFilesAccepted({ files, multiple: true, accept:'image/jpeg' })).toEqual(false) | ||
expect(utils.allFilesAccepted({ files: images, multiple: true,accept:'image/*' })).toEqual(true) | ||
expect(utils.allFilesAccepted({ files, multiple: true, accept:'image/jpeg' })).toEqual(false) | ||
expect(utils.allFilesAccepted({ files: images, multiple: true,accept:'image/*' })).toEqual(true) | ||
expect(utils.allFilesAccepted({ files, multiple: true, minSize: 110 })).toEqual(false) | ||
@@ -261,17 +253,6 @@ expect(utils.allFilesAccepted({ files, multiple: true, maxSize: 99 })).toEqual(false) | ||
function createFile(name, size, type) { | ||
const file = new File([], name, { type }) | ||
Object.defineProperty(file, 'size', { | ||
get() { | ||
return size | ||
} | ||
}) | ||
return file | ||
} | ||
describe('ErrorCode', () => { | ||
let utils | ||
beforeEach(async done => { | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
done() | ||
}) | ||
@@ -287,1 +268,83 @@ | ||
describe('canUseFileSystemAccessAPI()', () => { | ||
let utils | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
}) | ||
it('should return false if not', () => { | ||
expect(utils.canUseFileSystemAccessAPI()).toBe(false) | ||
}) | ||
it('should return true if yes', () => { | ||
// TODO: If we use these in other tests, restore once test is done | ||
window.showOpenFilePicker = jest.fn() | ||
expect(utils.canUseFileSystemAccessAPI()).toBe(true) | ||
}) | ||
}) | ||
describe('filePickerOptionsTypes()', () => { | ||
let utils | ||
beforeEach(async () => { | ||
utils = await import('./index') | ||
}) | ||
it('should return proper types when the arg is a MIME type', () => { | ||
expect(utils.filePickerOptionsTypes('application/zip')).toEqual([{ | ||
description: 'everything', | ||
accept: {'application/zip': []} | ||
}]) | ||
}) | ||
it('should return proper types when the arg is an array of MIME types', () => { | ||
expect(utils.filePickerOptionsTypes(['application/zip', 'application/json'])).toEqual([{ | ||
description: 'everything', | ||
accept: { | ||
'application/zip': [], | ||
'application/json': [] | ||
} | ||
}]) | ||
}) | ||
it("should exclude anything that's not a MIME type", () => { | ||
expect(utils.filePickerOptionsTypes(['audio/*', 'video/*', 'image/*', '.txt', 'text/*'])).toEqual([{ | ||
description: 'everything', | ||
accept: { | ||
'audio/*': [], | ||
'video/*': [], | ||
'image/*': [], | ||
'text/*': [], | ||
} | ||
}]) | ||
}) | ||
it("should work with comma separated string of MIME types", () => { | ||
expect(utils.filePickerOptionsTypes('audio/*,video/*,image/*,.txt,text/*,application/zip')).toEqual([{ | ||
description: 'everything', | ||
accept: { | ||
'audio/*': [], | ||
'video/*': [], | ||
'image/*': [], | ||
'text/*': [], | ||
'application/zip': [] | ||
} | ||
}]) | ||
}) | ||
it('should return empty otherwise', () => { | ||
expect(utils.filePickerOptionsTypes('')).toEqual([{ | ||
description: 'everything', | ||
accept: {} | ||
}]) | ||
}) | ||
}) | ||
function createFile(name, size, type) { | ||
const file = new File([], name, { type }) | ||
Object.defineProperty(file, 'size', { | ||
get() { | ||
return size | ||
} | ||
}) | ||
return file | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
387862
49
5890
431
66
+ Addedfile-selector@0.4.0(transitive)
- Removedfile-selector@0.2.4(transitive)
Updatedattr-accept@^2.2.2
Updatedfile-selector@^0.4.0
Updatedprop-types@^15.8.1