{
+ markTouched();
+ onBlur?.(event);
+ }}
+ >
+
+ {files.length === 0 ? (
+ renderEmptyState()
+ ) : (
+
+
+ {files.map((file, index) => (
+ -
+ handleRemove(index)}
+ />
+
+ ))}
+
+
+
+ {captionElement}
+
+
+ )}
+
+ {fullScreenActive ? (
+
+ ) : null}
+ {caption ?
{caption}
: null}
+ {invalid && errorMessage ? (
+
{errorMessage}
+ ) : null}
+
+ );
+});
+
+MultipleFileUpload.displayName = 'MultipleFileUpload';
diff --git a/packages/components/src/components/FileUpload/SingleFileUpload.module.css b/packages/components/src/components/FileUpload/SingleFileUpload.module.css
new file mode 100644
index 00000000..9af4bf8f
--- /dev/null
+++ b/packages/components/src/components/FileUpload/SingleFileUpload.module.css
@@ -0,0 +1,7 @@
+/* Single upload only tunes the shared drop-area via custom properties. The
+ container padding matches the Angular single container (12px/16px minus the
+ 1px dashed border → 11px/15px). */
+.dropArea {
+ --fileupload-drop-padding-block: calc(var(--kbq-size-m) - 1px);
+ --fileupload-drop-padding-inline: calc(var(--kbq-size-l) - 1px);
+}
diff --git a/packages/components/src/components/FileUpload/SingleFileUpload.test.tsx b/packages/components/src/components/FileUpload/SingleFileUpload.test.tsx
new file mode 100644
index 00000000..efd5607e
--- /dev/null
+++ b/packages/components/src/components/FileUpload/SingleFileUpload.test.tsx
@@ -0,0 +1,188 @@
+import {
+ cleanup,
+ fireEvent,
+ render,
+ screen,
+ waitFor,
+} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { SingleFileUpload } from './SingleFileUpload';
+import type { FileItem } from './types';
+
+afterEach(cleanup);
+
+const makeItem = (name = 'document.txt'): FileItem => ({
+ file: new File(['hello world'], name, { type: 'text/plain' }),
+ loading: false,
+ progress: 0,
+});
+
+const getFileInput = (): HTMLInputElement =>
+ document.querySelector('input[type="file"]') as HTMLInputElement;
+
+const getDropArea = (container: HTMLElement): HTMLElement =>
+ container.querySelector('[class*="dropArea"]') as HTMLElement;
+
+const dataTransferWith = (files: File[]) =>
+ ({
+ items: files.map((file) => ({
+ kind: 'file',
+ webkitGetAsEntry: () => ({
+ isFile: true,
+ isDirectory: false,
+ fullPath: `/${file.name}`,
+ file: (cb: (f: File) => void) => cb(file),
+ }),
+ })),
+ files,
+ }) as unknown as DataTransfer;
+
+describe('SingleFileUpload', () => {
+ it('renders an empty state with a browse link', () => {
+ render(