@availity/mui-file-selector 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -114,6 +114,66 @@ var FilePickerBtn = (_a) => {
114
114
  ] });
115
115
  };
116
116
 
117
+ // src/lib/util.ts
118
+ import {
119
+ FileArchiveIcon,
120
+ FileCodeIcon,
121
+ FileCsvIcon,
122
+ FileExcelIcon,
123
+ FileIcon,
124
+ FileImageIcon,
125
+ FileLinesIcon,
126
+ FilePdfIcon,
127
+ FilePowerpointIcon,
128
+ FileWordIcon
129
+ } from "@availity/mui-icon";
130
+ function formatBytes(bytes, decimals = 2) {
131
+ if (!+bytes) return "0 Bytes";
132
+ const k = 1024;
133
+ const dm = decimals < 0 ? 0 : decimals;
134
+ const sizes = ["Bytes", "KB", "MB", "GB"];
135
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
136
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
137
+ }
138
+ var FILE_EXT_ICONS = {
139
+ png: FileImageIcon,
140
+ jpg: FileImageIcon,
141
+ jpeg: FileImageIcon,
142
+ gif: FileImageIcon,
143
+ csv: FileCsvIcon,
144
+ ppt: FilePowerpointIcon,
145
+ pptx: FilePowerpointIcon,
146
+ xls: FileExcelIcon,
147
+ xlsx: FileExcelIcon,
148
+ doc: FileWordIcon,
149
+ docx: FileWordIcon,
150
+ txt: FileLinesIcon,
151
+ text: FileLinesIcon,
152
+ zip: FileArchiveIcon,
153
+ "7zip": FileArchiveIcon,
154
+ xml: FileCodeIcon,
155
+ html: FileCodeIcon,
156
+ pdf: FilePdfIcon
157
+ };
158
+ var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
159
+ var getFileExtension = (fileName) => {
160
+ var _a;
161
+ return ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
162
+ };
163
+ var getFileExtIcon = (fileName) => {
164
+ const ext = getFileExtension(fileName);
165
+ return isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
166
+ };
167
+ var dedupeErrors = (errors) => {
168
+ const dedupedErrors = errors.reduce((acc, error) => {
169
+ if (!acc.find((err) => err.code === error.code)) {
170
+ acc.push(error);
171
+ }
172
+ return acc;
173
+ }, []);
174
+ return dedupedErrors;
175
+ };
176
+
117
177
  // src/lib/Dropzone.tsx
118
178
  import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
119
179
  var outerBoxStyles = {
@@ -153,6 +213,7 @@ var Dropzone = ({
153
213
  enableDropArea = true,
154
214
  maxFiles,
155
215
  maxSize,
216
+ maxTotalSize,
156
217
  multiple,
157
218
  name,
158
219
  onChange,
@@ -205,7 +266,48 @@ var Dropzone = ({
205
266
  }
206
267
  setTotalSize((prev) => prev + newSize);
207
268
  const previous = (_a2 = watch(name)) != null ? _a2 : [];
208
- setValue(name, previous.concat(acceptedFiles));
269
+ if (maxTotalSize) {
270
+ const currentTotalSize = previous.reduce((sum, file) => sum + file.size, 0);
271
+ let newSize2 = 0;
272
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
273
+ let sizeCounter = 0;
274
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
275
+ sizeCounter += file.size;
276
+ return sizeCounter > availableSize;
277
+ });
278
+ if (cutoffIndex !== -1) {
279
+ const filesToAdd2 = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
280
+ fileRejections.push({
281
+ file: acceptedFiles[cutoffIndex],
282
+ errors: [
283
+ {
284
+ code: "upload-too-large",
285
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`
286
+ }
287
+ ],
288
+ id: counter.increment()
289
+ });
290
+ acceptedFiles = filesToAdd2;
291
+ }
292
+ newSize2 = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
293
+ setTotalSize((prev) => prev + newSize2);
294
+ }
295
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
296
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
297
+ setValue(name, previous.concat(filesToAdd));
298
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
299
+ fileRejections.push({
300
+ file: acceptedFiles[remainingSlots],
301
+ // Use the first excess file
302
+ errors: [
303
+ {
304
+ code: "too-many-files",
305
+ message: `Too many files. You may only upload ${maxFiles} file(s).`
306
+ }
307
+ ],
308
+ id: counter.increment()
309
+ });
310
+ }
209
311
  if (fileRejections.length > 0) {
210
312
  const TOO_MANY_FILES_CODE = "too-many-files";
211
313
  let hasTooManyFiles = false;
@@ -306,68 +408,6 @@ import { CloudUploadIcon as CloudUploadIcon2, PlusIcon as PlusIcon2 } from "@ava
306
408
  import { Box as Box2, Stack as Stack2 } from "@availity/mui-layout";
307
409
  import { Typography as Typography2 } from "@availity/mui-typography";
308
410
  import Upload from "@availity/upload-core";
309
-
310
- // src/lib/util.ts
311
- import {
312
- FileArchiveIcon,
313
- FileCodeIcon,
314
- FileCsvIcon,
315
- FileExcelIcon,
316
- FileIcon,
317
- FileImageIcon,
318
- FileLinesIcon,
319
- FilePdfIcon,
320
- FilePowerpointIcon,
321
- FileWordIcon
322
- } from "@availity/mui-icon";
323
- function formatBytes(bytes, decimals = 2) {
324
- if (!+bytes) return "0 Bytes";
325
- const k = 1024;
326
- const dm = decimals < 0 ? 0 : decimals;
327
- const sizes = ["Bytes", "KB", "MB", "GB"];
328
- const i = Math.floor(Math.log(bytes) / Math.log(k));
329
- return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`;
330
- }
331
- var FILE_EXT_ICONS = {
332
- png: FileImageIcon,
333
- jpg: FileImageIcon,
334
- jpeg: FileImageIcon,
335
- gif: FileImageIcon,
336
- csv: FileCsvIcon,
337
- ppt: FilePowerpointIcon,
338
- pptx: FilePowerpointIcon,
339
- xls: FileExcelIcon,
340
- xlsx: FileExcelIcon,
341
- doc: FileWordIcon,
342
- docx: FileWordIcon,
343
- txt: FileLinesIcon,
344
- text: FileLinesIcon,
345
- zip: FileArchiveIcon,
346
- "7zip": FileArchiveIcon,
347
- xml: FileCodeIcon,
348
- html: FileCodeIcon,
349
- pdf: FilePdfIcon
350
- };
351
- var isValidKey = (key) => key ? key in FILE_EXT_ICONS : false;
352
- var getFileExtension = (fileName) => {
353
- var _a;
354
- return ((_a = fileName.split(".").pop()) == null ? void 0 : _a.toLowerCase()) || "";
355
- };
356
- var getFileExtIcon = (fileName) => {
357
- const ext = getFileExtension(fileName);
358
- return isValidKey(ext) ? FILE_EXT_ICONS[ext] : FileIcon;
359
- };
360
- var dedupeErrors = (errors) => {
361
- const dedupedErrors = errors.reduce((acc, error) => {
362
- if (!acc.find((err) => err.code === error.code)) {
363
- acc.push(error);
364
- }
365
- return acc;
366
- }, []);
367
- return dedupedErrors;
368
- };
369
-
370
- // src/lib/Dropzone2.tsx
371
411
  import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
372
412
  var counter2 = createCounter();
373
413
  function startUpload(file, options) {
@@ -389,6 +429,7 @@ var Dropzone2 = ({
389
429
  enableDropArea = true,
390
430
  maxFiles,
391
431
  maxSize,
432
+ maxTotalSize,
392
433
  multiple,
393
434
  name,
394
435
  onChange,
@@ -443,8 +484,50 @@ var Dropzone2 = ({
443
484
  }
444
485
  setTotalSize((prev) => prev + newSize);
445
486
  const previous = (_a2 = watch(name)) != null ? _a2 : [];
446
- const uploads = acceptedFiles.map((file) => startUpload(file, uploadOptions));
487
+ if (maxTotalSize) {
488
+ const currentTotalSize = previous.reduce((sum, upload) => sum + upload.file.size, 0);
489
+ console.log({ previous });
490
+ let newSize2 = 0;
491
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
492
+ let sizeCounter = 0;
493
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
494
+ sizeCounter += file.size;
495
+ return sizeCounter > availableSize;
496
+ });
497
+ if (cutoffIndex !== -1) {
498
+ const filesToAdd2 = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
499
+ fileRejections.push({
500
+ file: acceptedFiles[cutoffIndex],
501
+ errors: [
502
+ {
503
+ code: "upload-too-large",
504
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`
505
+ }
506
+ ],
507
+ id: counter2.increment()
508
+ });
509
+ acceptedFiles = filesToAdd2;
510
+ }
511
+ newSize2 = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
512
+ setTotalSize((prev) => prev + newSize2);
513
+ }
514
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
515
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
516
+ const uploads = filesToAdd.map((file) => startUpload(file, uploadOptions));
447
517
  setValue(name, previous.concat(yield Promise.all(uploads)));
518
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
519
+ fileRejections.push({
520
+ file: acceptedFiles[remainingSlots],
521
+ // Use the first excess file
522
+ errors: [
523
+ {
524
+ code: "too-many-files",
525
+ message: `Too many files. You may only upload ${maxFiles} file(s).`
526
+ }
527
+ ],
528
+ id: counter2.increment()
529
+ });
530
+ }
448
531
  if (fileRejections.length > 0) {
449
532
  const TOO_MANY_FILES_CODE = "too-many-files";
450
533
  let hasTooManyFiles = false;
@@ -541,6 +624,7 @@ import { List, ListItem } from "@availity/mui-list";
541
624
  import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
542
625
  var codes = {
543
626
  "file-too-large": "File exceeds maximum size",
627
+ "upload-too-large": "File causes maximum total upload size to be exceeded",
544
628
  "file-invalid-type": "File has an invalid type",
545
629
  "file-too-small": "File is smaller than minimum size",
546
630
  "too-many-file": "Too many files",
@@ -1052,14 +1136,18 @@ import { jsxs as jsxs11 } from "react/jsx-runtime";
1052
1136
  var FileTypesMessage = ({
1053
1137
  allowedFileTypes = [],
1054
1138
  customSizeMessage,
1139
+ customTotalSizeMessage,
1055
1140
  customTypesMessage,
1056
1141
  maxFileSize,
1142
+ maxTotalSize,
1057
1143
  variant = "caption"
1058
1144
  }) => {
1059
1145
  const fileSizeMsg = customSizeMessage || (typeof maxFileSize === "number" ? `Maximum file size is ${formatBytes(maxFileSize)}. ` : null);
1146
+ const totalFileSizeMsg = customTotalSizeMessage || (typeof maxTotalSize === "number" ? `Maximum total upload size is ${formatBytes(maxTotalSize)}. ` : null);
1060
1147
  const fileTypesMsg = customTypesMessage || (allowedFileTypes.length > 0 ? `Supported file types include: ${allowedFileTypes.join(", ")}` : "All file types allowed.");
1061
1148
  return /* @__PURE__ */ jsxs11(Typography4, { variant, children: [
1062
1149
  fileSizeMsg,
1150
+ totalFileSizeMsg,
1063
1151
  fileTypesMsg
1064
1152
  ] });
1065
1153
  };
@@ -1067,12 +1155,14 @@ var FileTypesMessage = ({
1067
1155
  // src/lib/HeaderMessage.tsx
1068
1156
  import { Typography as Typography5 } from "@availity/mui-typography";
1069
1157
  import { jsxs as jsxs12 } from "react/jsx-runtime";
1070
- var HeaderMessage = ({ maxFiles, maxSize }) => {
1158
+ var HeaderMessage = ({ maxFiles, maxSize, maxTotalSize }) => {
1071
1159
  return /* @__PURE__ */ jsxs12(Typography5, { variant: "h6", children: [
1072
1160
  "Attach up to ",
1073
1161
  maxFiles,
1074
1162
  " file(s), with a maximum individual size of ",
1075
- formatBytes(maxSize)
1163
+ formatBytes(maxSize),
1164
+ " ",
1165
+ maxTotalSize && `and a maximum total size of ${formatBytes(maxTotalSize)}`
1076
1166
  ] });
1077
1167
  };
1078
1168
 
@@ -1087,6 +1177,7 @@ var FileSelector = ({
1087
1177
  clientId,
1088
1178
  children,
1089
1179
  customSizeMessage,
1180
+ customTotalSizeMessage,
1090
1181
  customTypesMessage,
1091
1182
  customerId,
1092
1183
  customFileRow,
@@ -1097,6 +1188,7 @@ var FileSelector = ({
1097
1188
  label = "Upload file",
1098
1189
  maxFiles,
1099
1190
  maxSize,
1191
+ maxTotalSize,
1100
1192
  multiple = true,
1101
1193
  onChange,
1102
1194
  onDrop,
@@ -1160,6 +1252,7 @@ var FileSelector = ({
1160
1252
  enableDropArea,
1161
1253
  maxFiles,
1162
1254
  maxSize,
1255
+ maxTotalSize,
1163
1256
  multiple,
1164
1257
  onChange,
1165
1258
  onDrop,
@@ -1173,7 +1266,9 @@ var FileSelector = ({
1173
1266
  {
1174
1267
  allowedFileTypes,
1175
1268
  maxFileSize: maxSize,
1269
+ maxTotalSize,
1176
1270
  customSizeMessage,
1271
+ customTotalSizeMessage,
1177
1272
  customTypesMessage,
1178
1273
  variant: "caption"
1179
1274
  }
@@ -1181,11 +1276,12 @@ var FileSelector = ({
1181
1276
  children
1182
1277
  ] }) : /* @__PURE__ */ jsxs13(Grid4, { container: true, rowSpacing: 3, flexDirection: "column", children: [
1183
1278
  /* @__PURE__ */ jsxs13(Grid4, { children: [
1184
- /* @__PURE__ */ jsx14(HeaderMessage, { maxFiles, maxSize }),
1279
+ /* @__PURE__ */ jsx14(HeaderMessage, { maxFiles, maxSize, maxTotalSize }),
1185
1280
  /* @__PURE__ */ jsx14(
1186
1281
  FileTypesMessage,
1187
1282
  {
1188
1283
  allowedFileTypes,
1284
+ customTotalSizeMessage,
1189
1285
  customSizeMessage,
1190
1286
  customTypesMessage,
1191
1287
  variant: "body2"
@@ -1202,6 +1298,7 @@ var FileSelector = ({
1202
1298
  enableDropArea,
1203
1299
  maxFiles,
1204
1300
  maxSize,
1301
+ maxTotalSize,
1205
1302
  multiple,
1206
1303
  onChange,
1207
1304
  onDrop,
@@ -1263,6 +1360,7 @@ var FileSelector2 = ({
1263
1360
  clientId,
1264
1361
  children,
1265
1362
  customSizeMessage,
1363
+ customTotalSizeMessage,
1266
1364
  customTypesMessage,
1267
1365
  customerId,
1268
1366
  customFileRow,
@@ -1273,6 +1371,7 @@ var FileSelector2 = ({
1273
1371
  label = "Upload file",
1274
1372
  maxFiles,
1275
1373
  maxSize,
1374
+ maxTotalSize,
1276
1375
  multiple = true,
1277
1376
  onChange,
1278
1377
  onDrop,
@@ -1333,6 +1432,7 @@ var FileSelector2 = ({
1333
1432
  enableDropArea,
1334
1433
  maxFiles,
1335
1434
  maxSize,
1435
+ maxTotalSize,
1336
1436
  multiple,
1337
1437
  onChange,
1338
1438
  onDrop,
@@ -1347,7 +1447,9 @@ var FileSelector2 = ({
1347
1447
  {
1348
1448
  allowedFileTypes,
1349
1449
  maxFileSize: maxSize,
1450
+ maxTotalSize,
1350
1451
  customSizeMessage,
1452
+ customTotalSizeMessage,
1351
1453
  customTypesMessage,
1352
1454
  variant: "caption"
1353
1455
  }
@@ -1355,12 +1457,13 @@ var FileSelector2 = ({
1355
1457
  children
1356
1458
  ] }) : /* @__PURE__ */ jsxs14(Grid5, { container: true, rowSpacing: 3, flexDirection: "column", children: [
1357
1459
  /* @__PURE__ */ jsxs14(Grid5, { children: [
1358
- /* @__PURE__ */ jsx15(HeaderMessage, { maxFiles, maxSize }),
1460
+ /* @__PURE__ */ jsx15(HeaderMessage, { maxFiles, maxSize, maxTotalSize }),
1359
1461
  /* @__PURE__ */ jsx15(
1360
1462
  FileTypesMessage,
1361
1463
  {
1362
1464
  allowedFileTypes,
1363
1465
  customSizeMessage,
1466
+ customTotalSizeMessage,
1364
1467
  customTypesMessage,
1365
1468
  variant: "body2"
1366
1469
  }
@@ -1376,6 +1479,7 @@ var FileSelector2 = ({
1376
1479
  enableDropArea,
1377
1480
  maxFiles,
1378
1481
  maxSize,
1482
+ maxTotalSize,
1379
1483
  multiple,
1380
1484
  onChange,
1381
1485
  onDrop,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@availity/mui-file-selector",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "description": "Availity MUI file-selector Component - part of the @availity/element design system",
5
5
  "keywords": [
6
6
  "react",
@@ -11,6 +11,7 @@ import { Box, Stack } from '@availity/mui-layout';
11
11
  import { Typography } from '@availity/mui-typography';
12
12
 
13
13
  import { FilePickerBtn } from './FilePickerBtn';
14
+ import { formatBytes } from './util';
14
15
 
15
16
  export const outerBoxStyles = {
16
17
  backgroundColor: 'background.secondary',
@@ -66,6 +67,10 @@ export type DropzoneProps = {
66
67
  * Maximum size of each file in bytes
67
68
  */
68
69
  maxSize?: number;
70
+ /**
71
+ * Maximum size of total upload in bytes
72
+ */
73
+ maxTotalSize?: number;
69
74
  /**
70
75
  * Whether multiple file selection is allowed
71
76
  */
@@ -110,6 +115,7 @@ export const Dropzone = ({
110
115
  enableDropArea = true,
111
116
  maxFiles,
112
117
  maxSize,
118
+ maxTotalSize,
113
119
  multiple,
114
120
  name,
115
121
  onChange,
@@ -169,8 +175,64 @@ export const Dropzone = ({
169
175
 
170
176
  const previous = watch(name) ?? [];
171
177
 
178
+ if (maxTotalSize) {
179
+ // Calculate current total size
180
+ const currentTotalSize = previous.reduce((sum: number, file: File) => sum + file.size, 0);
181
+ let newSize = 0;
182
+
183
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
184
+ let sizeCounter = 0;
185
+
186
+ // Find the index where we exceed the total size limit
187
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
188
+ sizeCounter += file.size;
189
+ return sizeCounter > availableSize;
190
+ });
191
+
192
+ // If we found files that exceed the limit
193
+ if (cutoffIndex !== -1) {
194
+ // Files that fit within the size limit
195
+ const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
196
+
197
+ // Create rejection for excess files
198
+ fileRejections.push({
199
+ file: acceptedFiles[cutoffIndex],
200
+ errors: [
201
+ {
202
+ code: 'upload-too-large',
203
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
204
+ },
205
+ ],
206
+ id: counter.increment(),
207
+ });
208
+
209
+ // Update acceptedFiles to only include files that fit
210
+ acceptedFiles = filesToAdd;
211
+ }
212
+
213
+ // Calculate size of accepted files for the state update
214
+ newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
215
+ setTotalSize((prev) => prev + newSize);
216
+ }
217
+
172
218
  // Set accepted files to form context
173
- setValue(name, previous.concat(acceptedFiles));
219
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
220
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
221
+ setValue(name, previous.concat(filesToAdd));
222
+
223
+ // Add rejections for excess files if needed
224
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
225
+ fileRejections.push({
226
+ file: acceptedFiles[remainingSlots], // Use the first excess file
227
+ errors: [
228
+ {
229
+ code: 'too-many-files',
230
+ message: `Too many files. You may only upload ${maxFiles} file(s).`,
231
+ },
232
+ ],
233
+ id: counter.increment(),
234
+ });
235
+ }
174
236
 
175
237
  if (fileRejections.length > 0) {
176
238
  const TOO_MANY_FILES_CODE = 'too-many-files';
@@ -12,7 +12,7 @@ import type { UploadOptions } from '@availity/upload-core';
12
12
  import type { OnSuccessPayload } from 'tus-js-client';
13
13
 
14
14
  import { FilePickerBtn } from './FilePickerBtn';
15
- import { dedupeErrors } from './util';
15
+ import { dedupeErrors, formatBytes } from './util';
16
16
  import { createCounter, DropzoneContainer, innerBoxStyles, outerBoxStyles } from './Dropzone';
17
17
  import type { DropzoneProps } from './Dropzone';
18
18
 
@@ -59,6 +59,7 @@ export const Dropzone2 = ({
59
59
  enableDropArea = true,
60
60
  maxFiles,
61
61
  maxSize,
62
+ maxTotalSize,
62
63
  multiple,
63
64
  name,
64
65
  onChange,
@@ -121,10 +122,67 @@ export const Dropzone2 = ({
121
122
 
122
123
  const previous = watch(name) ?? [];
123
124
 
125
+ if (maxTotalSize) {
126
+ // Calculate current total size
127
+ const currentTotalSize = previous.reduce((sum: number, upload: Upload) => sum + upload.file.size, 0);
128
+ console.log({ previous });
129
+ let newSize = 0;
130
+
131
+ const availableSize = Math.max(0, maxTotalSize - currentTotalSize);
132
+ let sizeCounter = 0;
133
+
134
+ // Find the index where we exceed the total size limit
135
+ const cutoffIndex = acceptedFiles.findIndex((file) => {
136
+ sizeCounter += file.size;
137
+ return sizeCounter > availableSize;
138
+ });
139
+
140
+ // If we found files that exceed the limit
141
+ if (cutoffIndex !== -1) {
142
+ // Files that fit within the size limit
143
+ const filesToAdd = acceptedFiles.slice(0, cutoffIndex === 0 ? 0 : cutoffIndex);
144
+
145
+ // Create rejection for excess files
146
+ fileRejections.push({
147
+ file: acceptedFiles[cutoffIndex],
148
+ errors: [
149
+ {
150
+ code: 'upload-too-large',
151
+ message: `Total upload size exceeds the limit of ${formatBytes(maxTotalSize)}.`,
152
+ },
153
+ ],
154
+ id: counter.increment(),
155
+ });
156
+
157
+ // Update acceptedFiles to only include files that fit
158
+ acceptedFiles = filesToAdd;
159
+ }
160
+
161
+ // Calculate size of accepted files for the state update
162
+ newSize = acceptedFiles.reduce((sum, file) => sum + file.size, 0);
163
+ setTotalSize((prev) => prev + newSize);
164
+ }
165
+
124
166
  // Set accepted files to form context
125
- const uploads = acceptedFiles.map((file) => startUpload(file, uploadOptions));
167
+ const remainingSlots = maxFiles ? Math.max(0, maxFiles - previous.length) : acceptedFiles.length;
168
+ const filesToAdd = acceptedFiles.slice(0, remainingSlots);
169
+ const uploads = filesToAdd.map((file) => startUpload(file, uploadOptions));
126
170
  setValue(name, previous.concat(await Promise.all(uploads)));
127
171
 
172
+ // Add rejections for excess files if needed
173
+ if (maxFiles && acceptedFiles.length > remainingSlots) {
174
+ fileRejections.push({
175
+ file: acceptedFiles[remainingSlots], // Use the first excess file
176
+ errors: [
177
+ {
178
+ code: 'too-many-files',
179
+ message: `Too many files. You may only upload ${maxFiles} file(s).`,
180
+ },
181
+ ],
182
+ id: counter.increment(),
183
+ });
184
+ }
185
+
128
186
  if (fileRejections.length > 0) {
129
187
  const TOO_MANY_FILES_CODE = 'too-many-files';
130
188
  let hasTooManyFiles = false;
@@ -5,6 +5,7 @@ import { formatBytes } from './util';
5
5
 
6
6
  const codes: Record<string, string> = {
7
7
  'file-too-large': 'File exceeds maximum size',
8
+ 'upload-too-large': 'File causes maximum total upload size to be exceeded',
8
9
  'file-invalid-type': 'File has an invalid type',
9
10
  'file-too-small': 'File is smaller than minimum size',
10
11
  'too-many-file': 'Too many files',
@@ -59,6 +59,11 @@ export type FileSelectorProps = {
59
59
  * Overrides the standard file size message
60
60
  */
61
61
  customSizeMessage?: React.ReactNode;
62
+
63
+ /**
64
+ * Overrides the standard total upload size message
65
+ */
66
+ customTotalSizeMessage?: React.ReactNode;
62
67
  /**
63
68
  * Overrides the standard file types message
64
69
  */
@@ -98,10 +103,16 @@ export type FileSelectorProps = {
98
103
  * Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
99
104
  */
100
105
  maxSize: number;
106
+ /**
107
+ * Maximum size allowed for total upload in bytes
108
+ * Use Kibi or Mibibytes. eg: 1kb = 1024 bytes; 1mb = 1024kb
109
+ */
110
+ maxTotalSize?: number;
101
111
  /**
102
112
  * Whether multiple file selection is allowed
103
113
  * @default true
104
114
  */
115
+
105
116
  multiple?: boolean;
106
117
  /**
107
118
  * Callback fired when files are selected
@@ -145,6 +156,7 @@ export const FileSelector = ({
145
156
  clientId,
146
157
  children,
147
158
  customSizeMessage,
159
+ customTotalSizeMessage,
148
160
  customTypesMessage,
149
161
  customerId,
150
162
  customFileRow,
@@ -155,6 +167,7 @@ export const FileSelector = ({
155
167
  label = 'Upload file',
156
168
  maxFiles,
157
169
  maxSize,
170
+ maxTotalSize,
158
171
  multiple = true,
159
172
  onChange,
160
173
  onDrop,
@@ -240,6 +253,7 @@ export const FileSelector = ({
240
253
  enableDropArea={enableDropArea}
241
254
  maxFiles={maxFiles}
242
255
  maxSize={maxSize}
256
+ maxTotalSize={maxTotalSize}
243
257
  multiple={multiple}
244
258
  onChange={onChange}
245
259
  onDrop={onDrop}
@@ -250,7 +264,9 @@ export const FileSelector = ({
250
264
  <FileTypesMessage
251
265
  allowedFileTypes={allowedFileTypes}
252
266
  maxFileSize={maxSize}
267
+ maxTotalSize={maxTotalSize}
253
268
  customSizeMessage={customSizeMessage}
269
+ customTotalSizeMessage={customTotalSizeMessage}
254
270
  customTypesMessage={customTypesMessage}
255
271
  variant="caption"
256
272
  />
@@ -259,9 +275,10 @@ export const FileSelector = ({
259
275
  ) : (
260
276
  <Grid container rowSpacing={3} flexDirection="column">
261
277
  <Grid>
262
- <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} />
278
+ <HeaderMessage maxFiles={maxFiles} maxSize={maxSize} maxTotalSize={maxTotalSize} />
263
279
  <FileTypesMessage
264
280
  allowedFileTypes={allowedFileTypes}
281
+ customTotalSizeMessage={customTotalSizeMessage}
265
282
  customSizeMessage={customSizeMessage}
266
283
  customTypesMessage={customTypesMessage}
267
284
  variant="body2"
@@ -276,6 +293,7 @@ export const FileSelector = ({
276
293
  enableDropArea={enableDropArea}
277
294
  maxFiles={maxFiles}
278
295
  maxSize={maxSize}
296
+ maxTotalSize={maxTotalSize}
279
297
  multiple={multiple}
280
298
  onChange={onChange}
281
299
  onDrop={onDrop}