@spark-web/dropzone
Version:
--- title: Drop Zone isExperimentalPackage: true ---
374 lines (349 loc) • 13.3 kB
JavaScript
import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
import _toConsumableArray from '@babel/runtime/helpers/esm/toConsumableArray';
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray';
import { css } from '@emotion/css';
import { useFocusRing, VisuallyHidden } from '@spark-web/a11y';
import { Alert } from '@spark-web/alert';
import { Box } from '@spark-web/box';
import { useFieldContext } from '@spark-web/field';
import { UploadIcon, DocumentTextIcon, XIcon } from '@spark-web/icon';
import { Stack } from '@spark-web/stack';
import { Text } from '@spark-web/text';
import { TextList } from '@spark-web/text-list';
import { useTheme } from '@spark-web/theme';
import { mergeRefs } from '@spark-web/utils';
import { forwardRef, useState, useEffect } from 'react';
import { useDropzone } from 'react-dropzone';
import { jsxs, jsx } from 'react/jsx-runtime';
// Add more MIME types as we need them:
var mimeTypeToFileExtension = {
'audio/*': 'audio files',
'audio/mpeg': '.mpeg',
'audio/wav': '.wav',
'image/*': 'image files',
'image/gif': '.gif',
'image/heic': '.heic',
'image/jpeg': '.jpg, .jpeg',
'image/png': '.png',
'image/svg+xml': '.svg+xml',
'image/tiff': '.tiff',
'image/webp': '.webp',
'text/*': 'text files',
'text/csv': '.csv',
'text/plain': '.plain',
'text/rtf': '.rtf',
'video/*': 'video files',
'video/mp4': '.mp4',
'video/mpeg': '.mpeg',
'application/msword': '.msword',
'application/pdf': '.pdf',
'application/rtf': '.rtf',
'application/vnd.ms-excel': '.xls',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx',
'application/zip': '.zip'
};
var _excluded = ["role", "tabIndex"],
_excluded2 = ["style"];
var Dropzone = /*#__PURE__*/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;
var _useState = useState([]),
_useState2 = _slicedToArray(_useState, 2),
files = _useState2[0],
setFiles = _useState2[1];
var _useState3 = 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 handleDropAccepted = function handleDropAccepted(acceptedFiles) {
var acceptedFilesWithPreview = acceptedFiles.map(function (acceptedFile, index) {
return _objectSpread(_objectSpread({}, acceptedFile), {}, {
id: files.length === 0 ? index + 1 : files.length + index + 1,
preview: acceptedFile.type.startsWith('image') ? URL.createObjectURL(acceptedFile) : undefined
});
});
setFiles(function (prevFiles) {
return [].concat(_toConsumableArray(prevFiles), _toConsumableArray(acceptedFilesWithPreview));
});
};
var _useFieldContext = useFieldContext(),
_useFieldContext2 = _slicedToArray(_useFieldContext, 2),
_useFieldContext2$ = _useFieldContext2[0],
disabled = _useFieldContext2$.disabled,
invalid = _useFieldContext2$.invalid,
a11yProps = _useFieldContext2[1];
var _useDropzone = useDropzone({
accept: accept,
maxFiles: maxFiles,
maxSize: maxFileSizeKb && maxFileSizeKb * 1000,
minSize: minFileSizeKb && minFileSizeKb * 1000,
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.
useEffect(function () {
onChange === null || onChange === void 0 ? void 0 : onChange({
target: {
value: files,
name: name
},
type: 'change'
});
onBlur === null || onBlur === void 0 ? void 0 : onBlur({
target: {
value: files,
name: name
},
type: 'blur'
});
}, [files, name, onBlur, onChange]);
useEffect(function () {
if (!fileRejections.length) {
setFileError(undefined);
return;
}
var errorMessage = {
errors: []
};
if (fileRejections.length > maxFiles) {
errorMessage.type = 'too-many-files';
errorMessage.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.'
}];
} else {
fileRejections.map(function (_ref2) {
var errors = _ref2.errors,
name = _ref2.file.name;
errors.forEach(function (error) {
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(Array.isArray(accept) ? accept.map(function (f) {
return mimeTypeToFileExtension[f];
}).join(', ') : mimeTypeToFileExtension[accept], ".");
break;
}
errorMessage.errors.push({
name: name,
message: message
});
});
});
}
setFileError(errorMessage);
}, [accept, fileRejections, maxFileSizeKb, maxFiles, minFileSizeKb]);
var isInvalid = invalid || isDragReject;
var theme = useTheme();
var focusRingStyles = useFocusRing();
var dropzoneStyles = useDropzoneStyles({
disabled: disabled,
isInvalid: isInvalid
});
return /*#__PURE__*/jsxs(Stack, {
gap: "large",
children: [/*#__PURE__*/jsx(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: mergeRefs([forwardedRef, dropzoneInputRef])
})), /*#__PURE__*/jsxs(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",
className: css(dropzoneStyles),
children: [isDragActive && /*#__PURE__*/jsx(Box // Position absolute so height never changes
, {
position: "absolute",
top: 0,
bottom: 0,
display: "flex",
alignItems: "center",
children: /*#__PURE__*/jsx(Text, {
tone: isDragReject ? 'critical' : 'neutral',
children: isDragReject ? 'file type not valid' : 'drop files to upload'
})
}), /*#__PURE__*/jsxs(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",
className: css(isDragActive ? {
opacity: 0,
pointerEvents: 'none'
} : null),
children: [/*#__PURE__*/jsx(UploadIcon, {
size: "medium",
tone: disabled ? 'disabled' : 'neutral'
}), /*#__PURE__*/jsxs(Text, {
align: "center",
tone: disabled ? 'disabled' : 'neutral',
children: ["click to select files ", /*#__PURE__*/jsx("br", {}), "or drop files here"]
})]
})]
})), fileError && /*#__PURE__*/jsx(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' ? /*#__PURE__*/jsx(Text, {
children: (_fileError$errors$ = fileError.errors[0]) === null || _fileError$errors$ === void 0 ? void 0 : _fileError$errors$.message
}) : /*#__PURE__*/jsx(TextList, {
gap: "medium",
children: fileError.errors.map(function (error) {
return /*#__PURE__*/jsxs(Text, {
weight: "regular",
children: [error.name, " - ", error.message]
}, error.name);
})
})
}), files.length > 0 && /*#__PURE__*/jsx(Stack, {
as: "ul",
role: "list",
gap: "medium",
children: files.map(function (file) {
return /*#__PURE__*/jsx(Box, {
as: "li",
border: "field",
borderRadius: "small",
padding: "xsmall",
children: /*#__PURE__*/jsxs(Box, {
display: "flex",
alignItems: "center",
gap: "medium",
height: "medium",
paddingLeft: "small",
children: [showImageThumbnails && file.preview ? /*#__PURE__*/jsx(Box, {
as: "img",
height: "xsmall",
width: "xsmall",
src: file.preview,
className: css({
objectFit: 'cover'
}),
alt: ""
}) : /*#__PURE__*/jsx(DocumentTextIcon, {
size: "xsmall"
}), /*#__PURE__*/jsx(Box, {
flex: 1,
children: /*#__PURE__*/jsx(Text, {
inline: true,
children: file.path
})
}), /*#__PURE__*/jsxs(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",
className: css({
transitionProperty: 'all',
transitionTimingFunction: theme.animation.standard.easing,
transitionDuration: "".concat(theme.animation.standard.duration, "ms"),
':hover': {
backgroundColor: theme.color.background.surfaceMuted
},
':focus': focusRingStyles
}),
children: [/*#__PURE__*/jsx(VisuallyHidden, {
children: "Remove file"
}), /*#__PURE__*/jsx(XIcon, {
size: "xxsmall"
})]
})]
})
}, file.id);
})
})]
});
});
Dropzone.displayName = 'Dropzone';
function useDropzoneStyles(_ref3) {
var disabled = _ref3.disabled,
isInvalid = _ref3.isInvalid;
var theme = useTheme();
var focusRingStyles = useFocusRing();
return {
borderStyle: 'dashed',
cursor: disabled ? 'default' : 'pointer',
transitionProperty: 'all',
transitionTimingFunction: theme.animation.standard.easing,
transitionDuration: "".concat(theme.animation.standard.duration, "ms"),
':hover': {
backgroundColor: disabled || isInvalid ? undefined : theme.color.background.infoLight,
borderColor: disabled || isInvalid ? undefined : theme.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");
}
export { Dropzone };