UNPKG

13.3 kBJavaScriptView Raw
1import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
2import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
3import _toConsumableArray from '@babel/runtime/helpers/esm/toConsumableArray';
4import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray';
5import { css } from '@emotion/css';
6import { useFocusRing, VisuallyHidden } from '@spark-web/a11y';
7import { Alert } from '@spark-web/alert';
8import { Box } from '@spark-web/box';
9import { useFieldContext } from '@spark-web/field';
10import { UploadIcon, DocumentTextIcon, XIcon } from '@spark-web/icon';
11import { Stack } from '@spark-web/stack';
12import { Text } from '@spark-web/text';
13import { TextList } from '@spark-web/text-list';
14import { useTheme } from '@spark-web/theme';
15import { mergeRefs } from '@spark-web/utils';
16import { forwardRef, useState, useMemo, useEffect } from 'react';
17import { useDropzone } from 'react-dropzone';
18import { jsxs, jsx } from 'react/jsx-runtime';
19
20var _excluded = ["role", "tabIndex"],
21 _excluded2 = ["style"];
22var Dropzone = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
23 var _fileError$errors$;
24 var accept = _ref.accept,
25 _ref$maxFiles = _ref.maxFiles,
26 maxFiles = _ref$maxFiles === void 0 ? 1 : _ref$maxFiles,
27 maxFileSizeKb = _ref.maxFileSizeKb,
28 minFileSizeKb = _ref.minFileSizeKb,
29 name = _ref.name,
30 onBlur = _ref.onBlur,
31 onChange = _ref.onChange,
32 showImageThumbnails = _ref.showImageThumbnails,
33 multiple = _ref.multiple;
34 var _useState = useState([]),
35 _useState2 = _slicedToArray(_useState, 2),
36 files = _useState2[0],
37 setFiles = _useState2[1];
38 var _useState3 = useState(),
39 _useState4 = _slicedToArray(_useState3, 2),
40 fileError = _useState4[0],
41 setFileError = _useState4[1];
42 var handleRemoveFile = function handleRemoveFile(id) {
43 setFiles(function (previousFiles) {
44 return previousFiles.filter(function (existingFile) {
45 return existingFile.id !== id;
46 });
47 });
48 };
49 var tooManyFilesErrorMessage = useMemo(function () {
50 return {
51 type: 'too-many-files',
52 errors: [{
53 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.'
54 }]
55 };
56 }, [maxFiles]);
57 var handleDropAccepted = function handleDropAccepted(acceptedFiles) {
58 var totalFiles = files.length + acceptedFiles.length;
59 if (maxFiles > 0 && totalFiles > maxFiles) {
60 setFileError(tooManyFilesErrorMessage);
61 return;
62 }
63 setFiles(function (previousFiles) {
64 return [].concat(_toConsumableArray(previousFiles), _toConsumableArray(acceptedFiles.map(function (acceptedFile, index) {
65 return Object.assign(acceptedFile, {
66 id: files.length === 0 ? index + 1 : files.length + index + 1,
67 preview: acceptedFile.type.startsWith('image') ? URL.createObjectURL(acceptedFile) : undefined
68 });
69 })));
70 });
71 };
72 var _useFieldContext = useFieldContext(),
73 _useFieldContext2 = _slicedToArray(_useFieldContext, 2),
74 _useFieldContext2$ = _useFieldContext2[0],
75 disabled = _useFieldContext2$.disabled,
76 invalid = _useFieldContext2$.invalid,
77 a11yProps = _useFieldContext2[1];
78 var _useDropzone = useDropzone({
79 accept: accept,
80 maxFiles: maxFiles,
81 maxSize: maxFileSizeKb && maxFileSizeKb * 1000,
82 minSize: minFileSizeKb && minFileSizeKb * 1000,
83 // If `multiple` is not provided, default to `true` if `maxFiles` is not 1
84 multiple: multiple !== undefined ? multiple : maxFiles !== 1,
85 onDropAccepted: handleDropAccepted,
86 disabled: disabled
87 }),
88 fileRejections = _useDropzone.fileRejections,
89 getInputProps = _useDropzone.getInputProps,
90 getRootProps = _useDropzone.getRootProps,
91 isDragActive = _useDropzone.isDragActive,
92 isDragReject = _useDropzone.isDragReject,
93 dropzoneInputRef = _useDropzone.inputRef;
94 var _getRootProps = getRootProps();
95 _getRootProps.role;
96 _getRootProps.tabIndex;
97 var dropzoneProps = _objectWithoutProperties(_getRootProps, _excluded);
98 var _getInputProps = getInputProps();
99 _getInputProps.style;
100 var dropzoneInputProps = _objectWithoutProperties(_getInputProps, _excluded2);
101
102 // HACK: Runs the `onChange` and `onBlur` functions and swaps in our local state whenever `files` is updated.
103 useEffect(function () {
104 onChange === null || onChange === void 0 || onChange({
105 target: {
106 value: files,
107 name: name
108 },
109 type: 'change'
110 });
111 onBlur === null || onBlur === void 0 || onBlur({
112 target: {
113 value: files,
114 name: name
115 },
116 type: 'blur'
117 });
118 }, [files, name, onBlur, onChange]);
119 useEffect(function () {
120 var errorMessage = {
121 errors: []
122 };
123 if (fileRejections.length < 1) {
124 return;
125 }
126 if (maxFiles > 0 && fileRejections.length > maxFiles) {
127 errorMessage.type = tooManyFilesErrorMessage.type;
128 errorMessage.errors = tooManyFilesErrorMessage.errors;
129 } else {
130 fileRejections.map(function (_ref2) {
131 var errors = _ref2.errors,
132 name = _ref2.file.name;
133 errors.forEach(function (error) {
134 var _Object$values;
135 var message = 'unknown validation error.';
136 switch (error.code) {
137 case 'file-too-large':
138 message = "is too large. Max supported file size is ".concat(formatFileSize(maxFileSizeKb || Infinity), ".");
139 break;
140 case 'file-too-small':
141 message = "is too small. Min supported file size is ".concat(formatFileSize(minFileSizeKb || 0), ".");
142 break;
143 case 'file-invalid-type':
144 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) {
145 return value.startsWith('.') ? value.substring(1) : value;
146 }).join(', '), ".");
147 break;
148 }
149 errorMessage.errors.push({
150 name: name,
151 message: message
152 });
153 });
154 });
155 }
156 setFileError(errorMessage);
157 }, [accept, fileRejections, maxFileSizeKb, maxFiles, minFileSizeKb, tooManyFilesErrorMessage]);
158 var isInvalid = invalid || isDragReject;
159 var theme = useTheme();
160 var focusRingStyles = useFocusRing();
161 var fileUploadError = !!(fileError !== null && fileError !== void 0 && fileError.errors.length);
162 var isMaxFilesReached = maxFiles > 0 && files.length === maxFiles;
163 var isDropzoneDisabled = disabled || fileUploadError || isMaxFilesReached;
164 var dropzoneStyles = useDropzoneStyles({
165 disabled: isDropzoneDisabled,
166 isInvalid: isInvalid
167 });
168 return /*#__PURE__*/jsxs(Stack, {
169 gap: "large",
170 children: [/*#__PURE__*/jsx(VisuallyHidden, _objectSpread(_objectSpread(_objectSpread({
171 as: "input",
172 disabled: disabled,
173 name: name,
174 onBlur: onBlur,
175 onChange: onChange
176 }, a11yProps), dropzoneInputProps), {}, {
177 // We need to forward the ref to the input so that libraries like `react-hook-form` will work.
178 // but `react-dropzone` also needs a ref, so we need to merge them.
179 ref: mergeRefs([forwardedRef, dropzoneInputRef])
180 })), /*#__PURE__*/jsxs(Stack, _objectSpread(_objectSpread({}, dropzoneProps), {}, {
181 as: "button",
182 align: "center",
183 background: function () {
184 if (disabled) return 'inputDisabled';
185 if (isInvalid) return 'criticalLight';
186 return 'surfaceMuted';
187 }(),
188 border: function () {
189 if (disabled) return 'fieldDisabled';
190 if (isInvalid) return 'critical';
191 return 'field';
192 }(),
193 borderRadius: "medium",
194 borderWidth: "large",
195 gap: "large",
196 padding: "large",
197 position: "relative",
198 className: css(dropzoneStyles),
199 disabled: isDropzoneDisabled,
200 children: [isDragActive && /*#__PURE__*/jsx(Box
201 // Position absolute so height never changes
202 , {
203 position: "absolute",
204 top: 0,
205 bottom: 0,
206 display: "flex",
207 alignItems: "center",
208 children: /*#__PURE__*/jsx(Text, {
209 tone: isDragReject ? 'critical' : 'neutral',
210 children: isDragReject ? 'file type not valid' : 'drop files to upload'
211 })
212 }), /*#__PURE__*/jsxs(Stack
213 // Hide from screen-readers and visually, but keep it in the document flow so height doesn't change
214 , {
215 "aria-hidden": isDragActive,
216 align: "center",
217 gap: "large",
218 position: "relative",
219 className: css(isDragActive ? {
220 opacity: 0,
221 pointerEvents: 'none'
222 } : null),
223 children: [/*#__PURE__*/jsx(UploadIcon, {
224 size: "medium",
225 tone: disabled ? 'disabled' : 'neutral'
226 }), /*#__PURE__*/jsxs(Text, {
227 align: "center",
228 tone: disabled ? 'disabled' : 'neutral',
229 children: ["click to select files ", /*#__PURE__*/jsx("br", {}), "or drop files here ", maxFiles > 0 ? "(max ".concat(maxFiles, ")") : '']
230 })]
231 })]
232 })), fileUploadError && /*#__PURE__*/jsx(Alert, {
233 tone: "critical",
234 heading: fileError.type === 'too-many-files' ? 'Maximum number of files reached' : 'These files couldn’t be added:',
235 closeLabel: "Dismiss alert",
236 onClose: function onClose() {
237 return setFileError(undefined);
238 },
239 children: fileError.type === 'too-many-files' ? /*#__PURE__*/jsx(Text, {
240 children: (_fileError$errors$ = fileError.errors[0]) === null || _fileError$errors$ === void 0 ? void 0 : _fileError$errors$.message
241 }) : /*#__PURE__*/jsx(TextList, {
242 gap: "medium",
243 children: fileError.errors.map(function (error) {
244 return /*#__PURE__*/jsxs(Text, {
245 weight: "regular",
246 children: [error.name, " - ", error.message]
247 }, error.name);
248 })
249 })
250 }), files.length > 0 && /*#__PURE__*/jsx(Stack, {
251 as: "ul",
252 role: "list",
253 gap: "medium",
254 children: files.map(function (file) {
255 return /*#__PURE__*/jsx(Box, {
256 as: "li",
257 border: "field",
258 borderRadius: "small",
259 padding: "xsmall",
260 children: /*#__PURE__*/jsxs(Box, {
261 display: "flex",
262 alignItems: "center",
263 gap: "medium",
264 height: "medium",
265 paddingLeft: "small",
266 children: [showImageThumbnails && file.preview ? /*#__PURE__*/jsx(Box, {
267 as: "img",
268 height: "xsmall",
269 width: "xsmall",
270 src: file.preview,
271 className: css({
272 objectFit: 'cover'
273 }),
274 alt: ""
275 }) : /*#__PURE__*/jsx(DocumentTextIcon, {
276 size: "xsmall"
277 }), /*#__PURE__*/jsx(Box, {
278 flex: 1,
279 children: /*#__PURE__*/jsx(Text, {
280 inline: true,
281 children: file.path
282 })
283 }), /*#__PURE__*/jsxs(Box, {
284 as: "button",
285 type: "button",
286 onClick: function onClick() {
287 return handleRemoveFile(file.id);
288 },
289 cursor: "pointer",
290 display: "flex",
291 alignItems: "center",
292 justifyContent: "center",
293 borderRadius: "small",
294 height: "medium",
295 width: "medium",
296 className: css({
297 transitionProperty: 'all',
298 transitionTimingFunction: theme.animation.standard.easing,
299 transitionDuration: "".concat(theme.animation.standard.duration, "ms"),
300 ':hover': {
301 backgroundColor: theme.color.background.surfaceMuted
302 },
303 ':focus': focusRingStyles
304 }),
305 children: [/*#__PURE__*/jsx(VisuallyHidden, {
306 children: "Remove file"
307 }), /*#__PURE__*/jsx(XIcon, {
308 size: "xxsmall"
309 })]
310 })]
311 })
312 }, file.id);
313 })
314 })]
315 });
316});
317Dropzone.displayName = 'Dropzone';
318function useDropzoneStyles(_ref3) {
319 var disabled = _ref3.disabled,
320 isInvalid = _ref3.isInvalid;
321 var theme = useTheme();
322 var focusRingStyles = useFocusRing();
323 return {
324 borderStyle: 'dashed',
325 cursor: disabled ? 'default' : 'pointer',
326 transitionProperty: 'all',
327 transitionTimingFunction: theme.animation.standard.easing,
328 transitionDuration: "".concat(theme.animation.standard.duration, "ms"),
329 ':hover': {
330 backgroundColor: disabled || isInvalid ? undefined : theme.color.background.infoLight,
331 borderColor: disabled || isInvalid ? undefined : theme.border.color.fieldHover
332 },
333 ':focus': focusRingStyles
334 };
335}
336function formatFileSize(numKb) {
337 if (numKb < 1000) {
338 return "".concat(Math.round(numKb).toFixed(), "kB");
339 }
340 return "".concat(Math.round(numKb / 1000).toFixed(), "MB");
341}
342
343export { Dropzone };