From c3c77f554fa0fa700cb68b6dff555b4076dd0b46 Mon Sep 17 00:00:00 2001
From: Airike Jaska <95303654+airikej@users.noreply.github.com>
Date: Wed, 30 Sep 2026 14:29:39 +0300
Subject: [PATCH 1/2] feat(file-upload,file-dropzone): rename reject prop and
switch max size to bytes #889
BREAKING CHANGE: validateIndividually renamed to keepRejectedFiles; maxSize is now in bytes.
---
component.manifest.json | 4 +++-
skills/tedi-react/references/forms.md | 8 ++++----
.../form/file-dropzone/file-dropzone.spec.tsx | 16 ++++++++--------
.../file-dropzone/file-dropzone.stories.tsx | 18 +++++++++---------
.../form/file-dropzone/file-dropzone.tsx | 8 ++++----
.../form/file-upload/file-upload.spec.tsx | 16 ++++++++--------
.../form/file-upload/file-upload.stories.tsx | 16 ++++++++--------
.../form/file-upload/file-upload.tsx | 12 +++++++-----
src/tedi/helpers/hooks/use-file-upload.ts | 19 +++++++++++--------
9 files changed, 62 insertions(+), 55 deletions(-)
diff --git a/component.manifest.json b/component.manifest.json
index 1fc17fd3f..070c42f3b 100644
--- a/component.manifest.json
+++ b/component.manifest.json
@@ -601,7 +601,9 @@
"name": "TableOfContents",
"category": "navigation",
"sourcePath": "src/tedi/components/navigation/table-of-contents",
- "status": [],
+ "status": [
+ "breakpointSupport"
+ ],
"description": "Navigational table of contents for long pages or multistep forms, composed from nestable TableOfContents.Item entries.",
"keyProps": [
"activeId",
diff --git a/skills/tedi-react/references/forms.md b/skills/tedi-react/references/forms.md
index 8fed13d50..3b47b2066 100644
--- a/skills/tedi-react/references/forms.md
+++ b/skills/tedi-react/references/forms.md
@@ -409,14 +409,14 @@ Multiple helpers:
```tsx
import { FileUpload, FileDropzone } from '@tedi-design-system/react/tedi';
-// Button-based upload (`maxSize` is in MB)
+// Button-based upload (`maxSize` is in bytes — e.g. `5 * 1024 ** 2` for 5 MB)
```
**Restrictions hint** — both components auto-render an "allowed types / max size" hint below the field. Hide it with `showRestrictions={false}` when the same info lives elsewhere (e.g. a `tooltip`); rejection error messages still render either way:
```tsx
-
+
```
**Rejections are reported and observable** — a file failing `accept`/`maxSize` (dragged *or* picked) surfaces a localised message, and `onChange` fires even when the drop is fully rejected (with the unchanged list), so single-file rejections aren't silent.
diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
index 13d06ef92..f645827d3 100644
--- a/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
+++ b/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
@@ -87,7 +87,7 @@ describe('FileDropzone', () => {
fileInputRef: { current: null },
});
- render();
+ render();
expect(screen.queryByText('Aggregate error')).not.toBeInTheDocument();
});
@@ -103,7 +103,7 @@ describe('FileDropzone', () => {
fileInputRef: { current: null },
});
- render();
+ render();
expect(screen.getByText('Aggregate error')).toBeInTheDocument();
});
@@ -124,7 +124,7 @@ describe('FileDropzone', () => {
id="3c"
name="file"
label="Upload File"
- validateIndividually
+ keepRejectedFiles
helper={{ type: 'error', text: 'Required' }}
/>
);
@@ -188,7 +188,7 @@ describe('FileDropzone', () => {
fileInputRef: { current: null },
});
- render();
+ render();
const useDropzoneMock = useDropzone as jest.Mock;
const dropzoneProps = useDropzoneMock.mock.calls[0][0];
@@ -324,9 +324,9 @@ describe('FileDropzone', () => {
name="docs"
label="Label"
accept=".pdf,.txt"
- maxSize={100}
+ maxSize={100 * 1024 ** 2}
multiple
- validateIndividually
+ keepRejectedFiles
files={[]}
defaultFiles={[]}
onChange={() => undefined}
@@ -336,8 +336,8 @@ describe('FileDropzone', () => {
);
const label = container.querySelector('label');
- ['accept', 'maxsize', 'files', 'name', 'defaultfiles', 'announcementtimeout', 'validateindividually'].forEach(
- (attr) => expect(label).not.toHaveAttribute(attr)
+ ['accept', 'maxsize', 'files', 'name', 'defaultfiles', 'announcementtimeout', 'keeprejectedfiles'].forEach((attr) =>
+ expect(label).not.toHaveAttribute(attr)
);
expect(container.querySelector('input[type="file"]')).toHaveAttribute('name', 'docs');
});
diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.stories.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.stories.tsx
index f4e3e1fef..4a25594f9 100644
--- a/src/tedi/components/form/file-dropzone/file-dropzone.stories.tsx
+++ b/src/tedi/components/form/file-dropzone/file-dropzone.stories.tsx
@@ -45,7 +45,7 @@ export const WithHint: Story = {
args: {
name: 'file',
accept: '.jpg,.png,.pdf',
- maxSize: 1,
+ maxSize: 1024 ** 2,
helper: {
text: 'JPG, PNG, PDF suurusega kuni 1 MB.',
},
@@ -59,7 +59,7 @@ export const Multiple: Story = {
name: 'file-multiple',
multiple: true,
accept: '.jpg,.png,.pdf',
- maxSize: 1,
+ maxSize: 1024 ** 2,
defaultFiles: [{ name: 'report.pdf' }, { name: 'report_1.pdf' }, { name: 'report_2.pdf' }],
helper: {
text: 'JPG, PNG, PDF suurusega kuni 1 MB.',
@@ -75,7 +75,7 @@ export const BatchValidation: Story = {
args: {
id: 'file-dropzone-batch-validation',
name: 'file-batch-validation',
- maxSize: 1,
+ maxSize: 1024 ** 2,
accept: '.pdf,.txt',
multiple: true,
defaultFiles: [{ id: '1', name: 'taotlus.pdf' }],
@@ -96,17 +96,17 @@ export const BatchValidation: Story = {
};
/**
- * `validateIndividually`: each file is validated separately and kept with its own valid/invalid
- * state, so the user can see and remove the ones that failed.
+ * `keepRejectedFiles` (needs `multiple`): files that fail validation stay in the list with their
+ * own invalid state instead of being discarded, so the user can see and remove the ones that failed.
*/
export const IndividualValidation: Story = {
args: {
id: 'file-dropzone-individual-validation',
name: 'file-individual-validation',
multiple: true,
- maxSize: 1,
+ maxSize: 1024 ** 2,
accept: '.pdf,.txt',
- validateIndividually: true,
+ keepRejectedFiles: true,
defaultFiles: [
{ id: '1', name: 'taotlus_scan_lk_1.pdf' },
{ id: '2', name: 'taotlus_scan_lk_2.pdf' },
@@ -143,7 +143,7 @@ export const WithAttachmentProps: Story = {
id: 'file-dropzone-attachment-props',
name: 'file-attachment-props',
multiple: true,
- maxSize: 200,
+ maxSize: 200 * 1024 ** 2,
defaultFiles: [
{ id: '1', name: 'arve_2026_06.pdf', size: 1_200_000 },
{ id: '2', name: 'aastaaruanne_2025.pdf', size: 5_400_000 },
@@ -200,7 +200,7 @@ export const States: Story = {
diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.tsx
index 52a894b96..3774cdee6 100644
--- a/src/tedi/components/form/file-dropzone/file-dropzone.tsx
+++ b/src/tedi/components/form/file-dropzone/file-dropzone.tsx
@@ -96,7 +96,7 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => {
accept,
maxSize,
multiple,
- validateIndividually,
+ keepRejectedFiles,
defaultFiles,
files,
onChange,
@@ -108,7 +108,7 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => {
accept,
maxSize,
multiple,
- validateIndividually,
+ keepRejectedFiles,
defaultFiles,
files,
onChange,
@@ -124,7 +124,7 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => {
disabled,
accept: toDropzoneAccept(accept),
multiple,
- maxSize: maxSize ? maxSize * 1024 ** 2 : undefined,
+ maxSize,
onDrop: (acceptedFiles, fileRejections = []) => {
if (disabled) return;
@@ -144,7 +144,7 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => {
},
});
- const autoError = validateIndividually && multiple ? undefined : errorHelper;
+ const autoError = keepRejectedFiles && multiple ? undefined : errorHelper;
const errorFeedback = autoError ?? (helper?.type === 'error' ? helper : undefined);
const hintFeedback = helper && helper.type !== 'error' ? helper : restrictionsHint;
diff --git a/src/tedi/components/form/file-upload/file-upload.spec.tsx b/src/tedi/components/form/file-upload/file-upload.spec.tsx
index 595163052..921c48296 100644
--- a/src/tedi/components/form/file-upload/file-upload.spec.tsx
+++ b/src/tedi/components/form/file-upload/file-upload.spec.tsx
@@ -18,7 +18,7 @@ describe('FileUpload component', () => {
onDelete: jest.fn(),
accept: '.jpg,.png',
multiple: true,
- maxSize: 5,
+ maxSize: 5 * 1024 ** 2,
label: 'Upload files',
};
@@ -75,8 +75,8 @@ describe('FileUpload component', () => {
render(
);
@@ -110,7 +110,7 @@ describe('FileUpload component', () => {
});
it('formats the max-size hint into readable units', () => {
- render();
+ render();
expect(screen.getByText(/file-upload.max-size 512 KB/i)).toBeInTheDocument();
});
@@ -205,7 +205,7 @@ describe('FileUpload component', () => {
});
it('should return helper text when accept and maxSize are provided', () => {
- const props = { ...defaultProps, accept: '.jpg,.png', maxSize: 5 };
+ const props = { ...defaultProps, accept: '.jpg,.png', maxSize: 5 * 1024 ** 2 };
render();
expect(screen.getByText(/file-upload.accept .jpg, .png/i)).toBeInTheDocument();
expect(screen.getByText(/file-upload.max-size 5 MB/i)).toBeInTheDocument();
@@ -305,8 +305,8 @@ describe('FileUpload component', () => {
expect(screen.getByText('test.jpg')).toBeInTheDocument();
});
- it('should add valid files individually when validateIndividually is true', () => {
- render();
+ it('should add valid files individually when keepRejectedFiles is true', () => {
+ render();
const input = screen.getByLabelText(/Upload files/i);
const validFile = new File(['dummy content'], 'test.jpg', { type: 'image/jpeg' });
const invalidFile = new File(['dummy content'], 'test.txt', { type: 'text/plain' });
@@ -338,7 +338,7 @@ describe('FileUpload component', () => {
});
it('should handle file size validation correctly', () => {
- render();
+ render();
const input = screen.getByLabelText(/Upload files/i);
const largeFile = new File(['a'.repeat(6 * 1024 * 1024)], 'large.jpg', {
diff --git a/src/tedi/components/form/file-upload/file-upload.stories.tsx b/src/tedi/components/form/file-upload/file-upload.stories.tsx
index c256e4a3c..33cc84902 100644
--- a/src/tedi/components/form/file-upload/file-upload.stories.tsx
+++ b/src/tedi/components/form/file-upload/file-upload.stories.tsx
@@ -92,10 +92,10 @@ export const ValidationFailed: Story = {
id: 'file-upload-validation-failed',
name: 'file-validation-failed',
label: 'Laadi fail üles',
- maxSize: 0.001,
+ maxSize: 1024,
accept: '.pdf,.txt',
multiple: true,
- validateIndividually: true,
+ keepRejectedFiles: true,
defaultFiles: [{ name: 'taotlus_scan_lk_1.pdf', isValid: false }],
},
render: (args) => (
@@ -114,10 +114,10 @@ export const ValidationSuccess: Story = {
id: 'file-upload-validation-failed',
name: 'file-validation-failed',
label: 'Laadi fail üles',
- maxSize: 0.001,
+ maxSize: 1024,
accept: '.pdf,.txt',
multiple: true,
- validateIndividually: true,
+ keepRejectedFiles: true,
defaultFiles: [{ name: 'taotlus_scan_lk_1.pdf', isValid: false }],
},
render: (args) => (
@@ -137,9 +137,9 @@ export const MultipleWithIndividualValidation: Story = {
name: 'file-multiple-individual-validation',
label: 'Laadi failid üles',
multiple: true,
- maxSize: 0.01,
+ maxSize: 10 * 1024,
accept: '.pdf,.txt',
- validateIndividually: true,
+ keepRejectedFiles: true,
hasClearButton: true,
defaultFiles: [
{ name: 'taotlus_scan_lk_1.pdf' },
@@ -228,7 +228,7 @@ export const SizeLimited: Story = {
id: 'file-upload-size-limited',
name: 'file-size-limited',
label: 'Laadi fail üles',
- maxSize: 0.001,
+ maxSize: 1024,
multiple: true,
},
};
@@ -238,7 +238,7 @@ export const ExtensionAndSizeLimit: Story = {
id: 'file-upload-size-extension-limited',
name: 'file-size-extension-limited',
label: 'Laadi fail üles',
- maxSize: 0.001,
+ maxSize: 1024,
accept: '.pdf,.txt',
multiple: true,
},
diff --git a/src/tedi/components/form/file-upload/file-upload.tsx b/src/tedi/components/form/file-upload/file-upload.tsx
index b78b8dd6d..e200a528d 100644
--- a/src/tedi/components/form/file-upload/file-upload.tsx
+++ b/src/tedi/components/form/file-upload/file-upload.tsx
@@ -86,13 +86,15 @@ export interface FileUploadProps extends Omit {
*/
disabled?: boolean;
/**
- * Maximum allowed file size in megabytes (MB).
+ * Maximum allowed file size in bytes (e.g. `5 * 1024 ** 2` for 5 MB).
*/
maxSize?: number;
/**
- * If `true`, validates each file separately instead of rejecting all at once.
+ * Only applies when `multiple` is set. When `true`, files that fail validation stay in the
+ * list marked invalid instead of being discarded; when `false`, only valid files are added.
+ * @default false
*/
- validateIndividually?: boolean;
+ keepRejectedFiles?: boolean;
/**
* Whether to show the auto-generated restrictions hint (allowed types / max size)
* below the field. Turn it off when the same info is shown elsewhere to avoid a
@@ -124,7 +126,7 @@ export const FileUpload = (props: FileUploadProps): JSX.Element => {
readOnly,
disabled = false,
maxSize,
- validateIndividually = false,
+ keepRejectedFiles = false,
showRestrictions,
size = 'default',
helper,
@@ -135,7 +137,7 @@ export const FileUpload = (props: FileUploadProps): JSX.Element => {
accept,
maxSize,
multiple,
- validateIndividually,
+ keepRejectedFiles,
defaultFiles,
onChange,
onDelete,
diff --git a/src/tedi/helpers/hooks/use-file-upload.ts b/src/tedi/helpers/hooks/use-file-upload.ts
index 7c12b69f9..f7498ed94 100644
--- a/src/tedi/helpers/hooks/use-file-upload.ts
+++ b/src/tedi/helpers/hooks/use-file-upload.ts
@@ -37,7 +37,7 @@ export interface UseFileUploadProps {
*/
accept?: string;
/**
- * The maximum file size allowed for upload, in megabytes (MB).
+ * The maximum file size allowed for upload, in bytes (e.g. `5 * 1024 ** 2` for 5 MB).
*/
maxSize?: number;
/**
@@ -46,9 +46,12 @@ export interface UseFileUploadProps {
*/
multiple?: boolean;
/**
- * If true, each file is validated separately instead of rejecting all at once.
+ * Only applies when `multiple` is set. When `true`, files that fail validation (wrong type
+ * or over `maxSize`) stay in the list marked invalid instead of being discarded; when
+ * `false`, only valid files are added. Files are validated individually either way.
+ * @default false
*/
- validateIndividually?: boolean;
+ keepRejectedFiles?: boolean;
/**
* An array of default files that are preloaded in the upload list.
*/
@@ -85,7 +88,7 @@ export interface UseFileUploadProps {
/**
* Formats a byte count into a readable size (`512 KB`, `2.5 MB`, …). Uses 1024-based
- * units to stay consistent with the size validation (`maxSize * 1024 ** 2`), and rolls
+ * units to stay consistent with the byte-based `maxSize` validation, and rolls
* sub-1-unit values down so `0.5 MB` reads as `512 KB` rather than `0.5MB` (#888).
*/
export const formatFileSize = (bytes: number): string => {
@@ -111,7 +114,7 @@ const getDefaultHelpers = (
const text = [
accept && `${getLabel('file-upload.accept')} ${accept.replaceAll(',', ', ')}`,
- maxSize && `${getLabel('file-upload.max-size')} ${formatFileSize(maxSize * 1024 ** 2)}`,
+ maxSize && `${getLabel('file-upload.max-size')} ${formatFileSize(maxSize)}`,
]
.filter(Boolean)
.join('. ');
@@ -133,7 +136,7 @@ export const useFileUpload = (props: UseFileUploadProps) => {
accept,
maxSize,
multiple = false,
- validateIndividually = false,
+ keepRejectedFiles = false,
defaultFiles = [],
onChange,
onDelete,
@@ -221,7 +224,7 @@ export const useFileUpload = (props: UseFileUploadProps) => {
const rejectedFiles: RejectedFile[] = [];
const uploadedFiles = filesArray.map((file) => {
const isValidExtension = validFileType(file);
- const isValidSize = !maxSize || file.size <= maxSize * 1024 ** 2;
+ const isValidSize = !maxSize || file.size <= maxSize;
if (!isValidExtension) rejectedFiles.push({ type: 'extension', file });
if (!isValidSize) rejectedFiles.push({ type: 'size', file });
@@ -259,7 +262,7 @@ export const useFileUpload = (props: UseFileUploadProps) => {
if (!multiple) {
newFiles = uploadedFiles.length > 0 && uploadedFiles[0].isValid ? [uploadedFiles[0]] : actualFiles;
- } else if (validateIndividually) {
+ } else if (keepRejectedFiles) {
const addable = dedupe(uploadedFiles);
newFiles = addable.length > 0 ? [...actualFiles, ...addable] : actualFiles;
} else {
From fcff4dadf4ec1ff05be7ae00505481b37da4a346 Mon Sep 17 00:00:00 2001
From: Airike Jaska <95303654+airikej@users.noreply.github.com>
Date: Fri, 2 Oct 2026 15:23:40 +0300
Subject: [PATCH 2/2] fix(file-upload,file-dropzone): keep rejected files for
single uploads #889
---
.../form/file-dropzone/file-dropzone.spec.tsx | 4 ++--
.../components/form/file-dropzone/file-dropzone.tsx | 2 +-
src/tedi/components/form/file-upload/file-upload.tsx | 5 +++--
src/tedi/helpers/hooks/use-file-upload.spec.tsx | 11 +++++++++++
src/tedi/helpers/hooks/use-file-upload.ts | 12 ++++++++----
5 files changed, 25 insertions(+), 9 deletions(-)
diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
index f645827d3..a99932161 100644
--- a/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
+++ b/src/tedi/components/form/file-dropzone/file-dropzone.spec.tsx
@@ -92,7 +92,7 @@ describe('FileDropzone', () => {
expect(screen.queryByText('Aggregate error')).not.toBeInTheDocument();
});
- it('still shows the aggregate error for single-file individual validation (rejected file is not listed)', () => {
+ it('suppresses the aggregate hook error for single-file keepRejectedFiles too (the kept file carries it)', () => {
mockUseFileUpload.mockReturnValue({
innerFiles: [],
errorHelper: { type: 'error', text: 'Aggregate error' },
@@ -105,7 +105,7 @@ describe('FileDropzone', () => {
render();
- expect(screen.getByText('Aggregate error')).toBeInTheDocument();
+ expect(screen.queryByText('Aggregate error')).not.toBeInTheDocument();
});
it('still renders a consumer-provided error helper under the dropzone with individual validation', () => {
diff --git a/src/tedi/components/form/file-dropzone/file-dropzone.tsx b/src/tedi/components/form/file-dropzone/file-dropzone.tsx
index 3774cdee6..407c7f91d 100644
--- a/src/tedi/components/form/file-dropzone/file-dropzone.tsx
+++ b/src/tedi/components/form/file-dropzone/file-dropzone.tsx
@@ -144,7 +144,7 @@ export const FileDropzone = (props: FileDropzoneProps): JSX.Element => {
},
});
- const autoError = keepRejectedFiles && multiple ? undefined : errorHelper;
+ const autoError = keepRejectedFiles ? undefined : errorHelper;
const errorFeedback = autoError ?? (helper?.type === 'error' ? helper : undefined);
const hintFeedback = helper && helper.type !== 'error' ? helper : restrictionsHint;
diff --git a/src/tedi/components/form/file-upload/file-upload.tsx b/src/tedi/components/form/file-upload/file-upload.tsx
index e200a528d..bfbae7eba 100644
--- a/src/tedi/components/form/file-upload/file-upload.tsx
+++ b/src/tedi/components/form/file-upload/file-upload.tsx
@@ -90,8 +90,9 @@ export interface FileUploadProps extends Omit {
*/
maxSize?: number;
/**
- * Only applies when `multiple` is set. When `true`, files that fail validation stay in the
- * list marked invalid instead of being discarded; when `false`, only valid files are added.
+ * When `true`, files that fail validation stay in the list marked invalid instead of being
+ * discarded, so the failing file is shown; when `false`, only valid files are added. Applies to
+ * both single and multiple uploads.
* @default false
*/
keepRejectedFiles?: boolean;
diff --git a/src/tedi/helpers/hooks/use-file-upload.spec.tsx b/src/tedi/helpers/hooks/use-file-upload.spec.tsx
index a8f0371ce..4cac41bfb 100644
--- a/src/tedi/helpers/hooks/use-file-upload.spec.tsx
+++ b/src/tedi/helpers/hooks/use-file-upload.spec.tsx
@@ -41,6 +41,17 @@ describe('useFileUpload — accept matching', () => {
expect(onChange).toHaveBeenLastCalledWith([]);
});
+
+ it('keeps a rejected single file (marked invalid) when keepRejectedFiles is set, even without multiple', () => {
+ const onChange = jest.fn();
+ const { result } = renderHook(() => useFileUpload({ accept: 'image/*', keepRejectedFiles: true, onChange }));
+
+ act(() => result.current.onFileChange(changeEvent([makeFile('doc.pdf', 'application/pdf')])));
+
+ expect(onChange).toHaveBeenLastCalledWith([expect.objectContaining({ name: 'doc.pdf', isValid: false })]);
+ expect(result.current.innerFiles).toHaveLength(1);
+ expect(result.current.errorHelper).toBeDefined();
+ });
});
describe('useFileUpload — onFileRemove error clearing', () => {
diff --git a/src/tedi/helpers/hooks/use-file-upload.ts b/src/tedi/helpers/hooks/use-file-upload.ts
index f7498ed94..259911e00 100644
--- a/src/tedi/helpers/hooks/use-file-upload.ts
+++ b/src/tedi/helpers/hooks/use-file-upload.ts
@@ -46,9 +46,10 @@ export interface UseFileUploadProps {
*/
multiple?: boolean;
/**
- * Only applies when `multiple` is set. When `true`, files that fail validation (wrong type
- * or over `maxSize`) stay in the list marked invalid instead of being discarded; when
- * `false`, only valid files are added. Files are validated individually either way.
+ * When `true`, files that fail validation (wrong type or over `maxSize`) stay in the list marked
+ * invalid instead of being discarded, so the failing file is shown under the dropzone; when
+ * `false`, only valid files are added. Applies to both single and multiple uploads; files are
+ * validated individually either way.
* @default false
*/
keepRejectedFiles?: boolean;
@@ -261,7 +262,10 @@ export const useFileUpload = (props: UseFileUploadProps) => {
let newFiles: FileUploadFile[];
if (!multiple) {
- newFiles = uploadedFiles.length > 0 && uploadedFiles[0].isValid ? [uploadedFiles[0]] : actualFiles;
+ // Single upload replaces the current file. Keep an invalid pick (shown under the dropzone)
+ // only when `keepRejectedFiles` is set; otherwise drop it and leave the list unchanged.
+ const [picked] = uploadedFiles;
+ newFiles = picked && (picked.isValid || keepRejectedFiles) ? [picked] : actualFiles;
} else if (keepRejectedFiles) {
const addable = dedupe(uploadedFiles);
newFiles = addable.length > 0 ? [...actualFiles, ...addable] : actualFiles;