UNPKG

@spark-web/dropzone

Version:

--- title: Drop Zone isExperimentalPackage: true ---

374 lines (349 loc) 13.3 kB
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 };