UNPKG

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