@spark-web/dropzone
Version:
--- title: Drop Zone isExperimentalPackage: true ---
348 lines (342 loc) • 13.4 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
var _toConsumableArray = require('@babel/runtime/helpers/toConsumableArray');
var _slicedToArray = require('@babel/runtime/helpers/slicedToArray');
var react$1 = require('@emotion/react');
var a11y = require('@spark-web/a11y');
var alert = require('@spark-web/alert');
var box = require('@spark-web/box');
var field = require('@spark-web/field');
var icon = require('@spark-web/icon');
var stack = require('@spark-web/stack');
var text = require('@spark-web/text');
var textList = require('@spark-web/text-list');
var theme = require('@spark-web/theme');
var utils = require('@spark-web/utils');
var react = require('react');
var reactDropzone = require('react-dropzone');
var jsxRuntime = require('@emotion/react/jsx-runtime');
var _excluded = ["role", "tabIndex"],
_excluded2 = ["style"];
var Dropzone = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
var _fileError$errors$;
var accept = _ref.accept,
_ref$maxFiles = _ref.maxFiles,
maxFiles = _ref$maxFiles === void 0 ? 1 : _ref$maxFiles,
maxFileSizeKb = _ref.maxFileSizeKb,
minFileSizeKb = _ref.minFileSizeKb,
name = _ref.name,
onBlur = _ref.onBlur,
onChange = _ref.onChange,
showImageThumbnails = _ref.showImageThumbnails,
multiple = _ref.multiple;
var _useState = react.useState([]),
_useState2 = _slicedToArray(_useState, 2),
files = _useState2[0],
setFiles = _useState2[1];
var _useState3 = react.useState(),
_useState4 = _slicedToArray(_useState3, 2),
fileError = _useState4[0],
setFileError = _useState4[1];
var handleRemoveFile = function handleRemoveFile(id) {
setFiles(function (previousFiles) {
return previousFiles.filter(function (existingFile) {
return existingFile.id !== id;
});
});
};
var tooManyFilesErrorMessage = react.useMemo(function () {
return {
type: 'too-many-files',
errors: [{
message: 'We can’t upload anymore files as there’s too many files. ' + "The maximum number of files is ".concat(maxFiles, ". ") + 'Please remove a file before trying again.'
}]
};
}, [maxFiles]);
var handleDropAccepted = function handleDropAccepted(acceptedFiles) {
var totalFiles = files.length + acceptedFiles.length;
if (maxFiles > 0 && totalFiles > maxFiles) {
setFileError(tooManyFilesErrorMessage);
return;
}
setFiles(function (previousFiles) {
return [].concat(_toConsumableArray(previousFiles), _toConsumableArray(acceptedFiles.map(function (acceptedFile, index) {
return Object.assign(acceptedFile, {
id: files.length === 0 ? index + 1 : files.length + index + 1,
preview: acceptedFile.type.startsWith('image') ? URL.createObjectURL(acceptedFile) : undefined
});
})));
});
};
var _useFieldContext = field.useFieldContext(),
_useFieldContext2 = _slicedToArray(_useFieldContext, 2),
_useFieldContext2$ = _useFieldContext2[0],
disabled = _useFieldContext2$.disabled,
invalid = _useFieldContext2$.invalid,
a11yProps = _useFieldContext2[1];
var _useDropzone = reactDropzone.useDropzone({
accept: accept,
maxFiles: maxFiles,
maxSize: maxFileSizeKb && maxFileSizeKb * 1000,
minSize: minFileSizeKb && minFileSizeKb * 1000,
// If `multiple` is not provided, default to `true` if `maxFiles` is not 1
multiple: multiple !== undefined ? multiple : maxFiles !== 1,
onDropAccepted: handleDropAccepted,
disabled: disabled
}),
fileRejections = _useDropzone.fileRejections,
getInputProps = _useDropzone.getInputProps,
getRootProps = _useDropzone.getRootProps,
isDragActive = _useDropzone.isDragActive,
isDragReject = _useDropzone.isDragReject,
dropzoneInputRef = _useDropzone.inputRef;
var _getRootProps = getRootProps();
_getRootProps.role;
_getRootProps.tabIndex;
var dropzoneProps = _objectWithoutProperties(_getRootProps, _excluded);
var _getInputProps = getInputProps();
_getInputProps.style;
var dropzoneInputProps = _objectWithoutProperties(_getInputProps, _excluded2);
// HACK: Runs the `onChange` and `onBlur` functions and swaps in our local state whenever `files` is updated.
react.useEffect(function () {
onChange === null || onChange === void 0 || onChange({
target: {
value: files,
name: name
},
type: 'change'
});
onBlur === null || onBlur === void 0 || onBlur({
target: {
value: files,
name: name
},
type: 'blur'
});
}, [files, name, onBlur, onChange]);
react.useEffect(function () {
var errorMessage = {
errors: []
};
if (fileRejections.length < 1) {
return;
}
if (maxFiles > 0 && fileRejections.length > maxFiles) {
errorMessage.type = tooManyFilesErrorMessage.type;
errorMessage.errors = tooManyFilesErrorMessage.errors;
} else {
fileRejections.map(function (_ref2) {
var errors = _ref2.errors,
name = _ref2.file.name;
errors.forEach(function (error) {
var _Object$values;
var message = 'unknown validation error.';
switch (error.code) {
case 'file-too-large':
message = "is too large. Max supported file size is ".concat(formatFileSize(maxFileSizeKb || Infinity), ".");
break;
case 'file-too-small':
message = "is too small. Min supported file size is ".concat(formatFileSize(minFileSizeKb || 0), ".");
break;
case 'file-invalid-type':
message = "is not a supported file type. Supported file types are ".concat((_Object$values = Object.values(accept !== null && accept !== void 0 ? accept : {})) === null || _Object$values === void 0 ? void 0 : _Object$values.flat().map(function (value) {
return value.startsWith('.') ? value.substring(1) : value;
}).join(', '), ".");
break;
}
errorMessage.errors.push({
name: name,
message: message
});
});
});
}
setFileError(errorMessage);
}, [accept, fileRejections, maxFileSizeKb, maxFiles, minFileSizeKb, tooManyFilesErrorMessage]);
var isInvalid = invalid || isDragReject;
var theme$1 = theme.useTheme();
var focusRingStyles = a11y.useFocusRing();
var fileUploadError = !!(fileError !== null && fileError !== void 0 && fileError.errors.length);
var isMaxFilesReached = maxFiles > 0 && files.length === maxFiles;
var isDropzoneDisabled = disabled || fileUploadError || isMaxFilesReached;
var dropzoneStyles = useDropzoneStyles({
disabled: isDropzoneDisabled,
isInvalid: isInvalid
});
return jsxRuntime.jsxs(stack.Stack, {
gap: "large",
children: [jsxRuntime.jsx(a11y.VisuallyHidden, _objectSpread(_objectSpread(_objectSpread({
as: "input",
disabled: disabled,
name: name,
onBlur: onBlur,
onChange: onChange
}, a11yProps), dropzoneInputProps), {}, {
// We need to forward the ref to the input so that libraries like `react-hook-form` will work.
// but `react-dropzone` also needs a ref, so we need to merge them.
ref: utils.mergeRefs([forwardedRef, dropzoneInputRef])
})), jsxRuntime.jsxs(stack.Stack, _objectSpread(_objectSpread({}, dropzoneProps), {}, {
as: "button",
align: "center",
background: function () {
if (disabled) return 'inputDisabled';
if (isInvalid) return 'criticalLight';
return 'surfaceMuted';
}(),
border: function () {
if (disabled) return 'fieldDisabled';
if (isInvalid) return 'critical';
return 'field';
}(),
borderRadius: "medium",
borderWidth: "large",
gap: "large",
padding: "large",
position: "relative",
css: react$1.css(dropzoneStyles),
disabled: isDropzoneDisabled,
children: [isDragActive && jsxRuntime.jsx(box.Box
// Position absolute so height never changes
, {
position: "absolute",
top: 0,
bottom: 0,
display: "flex",
alignItems: "center",
children: jsxRuntime.jsx(text.Text, {
tone: isDragReject ? 'critical' : 'neutral',
children: isDragReject ? 'file type not valid' : 'drop files to upload'
})
}), jsxRuntime.jsxs(stack.Stack
// Hide from screen-readers and visually, but keep it in the document flow so height doesn't change
, {
"aria-hidden": isDragActive,
align: "center",
gap: "large",
position: "relative",
css: react$1.css(isDragActive ? {
opacity: 0,
pointerEvents: 'none'
} : null),
children: [jsxRuntime.jsx(icon.UploadIcon, {
size: "medium",
tone: disabled ? 'disabled' : 'neutral'
}), jsxRuntime.jsxs(text.Text, {
align: "center",
tone: disabled ? 'disabled' : 'neutral',
children: ["click to select files ", jsxRuntime.jsx("br", {}), "or drop files here ", maxFiles > 0 ? "(max ".concat(maxFiles, ")") : '']
})]
})]
})), fileUploadError && jsxRuntime.jsx(alert.Alert, {
tone: "critical",
heading: fileError.type === 'too-many-files' ? 'Maximum number of files reached' : 'These files couldn’t be added:',
closeLabel: "Dismiss alert",
onClose: function onClose() {
return setFileError(undefined);
},
children: fileError.type === 'too-many-files' ? jsxRuntime.jsx(text.Text, {
children: (_fileError$errors$ = fileError.errors[0]) === null || _fileError$errors$ === void 0 ? void 0 : _fileError$errors$.message
}) : jsxRuntime.jsx(textList.TextList, {
gap: "medium",
children: fileError.errors.map(function (error) {
return jsxRuntime.jsxs(text.Text, {
weight: "regular",
children: [error.name, " - ", error.message]
}, error.name);
})
})
}), files.length > 0 && jsxRuntime.jsx(stack.Stack, {
as: "ul",
role: "list",
gap: "medium",
children: files.map(function (file) {
return jsxRuntime.jsx(box.Box, {
as: "li",
border: "field",
borderRadius: "small",
padding: "xsmall",
children: jsxRuntime.jsxs(box.Box, {
display: "flex",
alignItems: "center",
gap: "medium",
height: "medium",
paddingLeft: "small",
children: [showImageThumbnails && file.preview ? jsxRuntime.jsx(box.Box, {
as: "img",
height: "xsmall",
width: "xsmall",
src: file.preview,
css: react$1.css({
objectFit: 'cover'
}),
alt: ""
}) : jsxRuntime.jsx(icon.DocumentTextIcon, {
size: "xsmall"
}), jsxRuntime.jsx(box.Box, {
flex: 1,
children: jsxRuntime.jsx(text.Text, {
inline: true,
children: file.path
})
}), jsxRuntime.jsxs(box.Box, {
as: "button",
type: "button",
onClick: function onClick() {
return handleRemoveFile(file.id);
},
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "small",
height: "medium",
width: "medium",
css: react$1.css({
transitionProperty: 'all',
transitionTimingFunction: theme$1.animation.standard.easing,
transitionDuration: "".concat(theme$1.animation.standard.duration, "ms"),
':hover': {
backgroundColor: theme$1.color.background.surfaceMuted
},
':focus': focusRingStyles
}),
children: [jsxRuntime.jsx(a11y.VisuallyHidden, {
children: "Remove file"
}), jsxRuntime.jsx(icon.XIcon, {
size: "xxsmall"
})]
})]
})
}, file.id);
})
})]
});
});
Dropzone.displayName = 'Dropzone';
function useDropzoneStyles(_ref3) {
var disabled = _ref3.disabled,
isInvalid = _ref3.isInvalid;
var theme$1 = theme.useTheme();
var focusRingStyles = a11y.useFocusRing();
return {
borderStyle: 'dashed',
cursor: disabled ? 'default' : 'pointer',
transitionProperty: 'all',
transitionTimingFunction: theme$1.animation.standard.easing,
transitionDuration: "".concat(theme$1.animation.standard.duration, "ms"),
':hover': {
backgroundColor: disabled || isInvalid ? undefined : theme$1.color.background.infoLight,
borderColor: disabled || isInvalid ? undefined : theme$1.border.color.fieldHover
},
':focus': focusRingStyles
};
}
function formatFileSize(numKb) {
if (numKb < 1000) {
return "".concat(Math.round(numKb).toFixed(), "kB");
}
return "".concat(Math.round(numKb / 1000).toFixed(), "MB");
}
exports.Dropzone = Dropzone;