File uploader: The compact layout
Set variant="compact" and the file uploader is a Browse button and the limits on one line, with no drop zone, and the files listed under it. Use it in tight forms, dialogs and toolbars, where a large drop area does not fit.
.pdf, .docx · up to 10 MB · max 3 files
import { FileUploader } from "slotsmith/file-uploader";
import { fakeUpload } from "../shared/fake-upload";
export default function Compact() {
return (
<div style={{ display: "grid", gap: "0.5rem" }}>
<p aria-hidden="true">Attachments</p>
{/* A Browse button and the hint on one line, the files listed under it: no drop zone. */}
<FileUploader
variant="compact"
multiple
accept=".pdf,.docx"
maxFiles={3}
maxSize={10 * 1024 * 1024}
upload={fakeUpload}
labels={{ dropzone: "Attachments", browse: "Attach files" }}
/>
</div>
);
}/** What the uploader hands an `upload` function, as far as this stand-in reads it. */
export interface FakeUploadContext {
signal: AbortSignal;
onProgress(percent: number): void;
}
/** Five steps of 200 ms: the whole upload takes one second. */
const STEPS = 5;
const STEP_MS = 200;
/** Waits `ms`, and rejects with an `AbortError` as soon as the signal aborts. */
function wait(ms: number, signal: AbortSignal): Promise<void> {
return new Promise((resolve, reject) => {
let timer: ReturnType<typeof setTimeout> | undefined;
const abort = () => {
clearTimeout(timer);
reject(new DOMException("The upload was cancelled.", "AbortError"));
};
if (signal.aborted) return abort();
timer = setTimeout(() => {
signal.removeEventListener("abort", abort);
resolve();
}, ms);
signal.addEventListener("abort", abort, { once: true });
});
}
/**
* A stand-in for an upload endpoint. Nothing leaves the browser: it reports
* progress in five steps over one second, stops as soon as `signal` aborts,
* and resolves with `/samples/uploaded/<name>`. With `fail`, it fails at 60%
* the way a server error would, so the retry path can be tried.
*/
export async function fakeUpload(
file: File,
{ signal, onProgress }: FakeUploadContext,
{ fail = false }: { fail?: boolean } = {},
): Promise<{ url: string }> {
for (let step = 1; step <= STEPS; step += 1) {
await wait(STEP_MS, signal);
if (fail && step === 3) throw new Error("The server answered 503. Try again.");
onProgress((step / STEPS) * 100);
}
return { url: `/samples/uploaded/${encodeURIComponent(file.name)}` };
}Attach a PDF: it is listed under the button and uploads as in the other layouts.
What changes
- The root carries
data-variant="compact", for your own styles. - There is no drop zone, so nothing takes a drop and no element has
role="button". The Browse button, theTriggerslot, opens the file dialog. - The hint beside the button lists the limits, as in the drop zone.
- The list, the progress, the actions and the refused files work as in the default layout.
Under the hood, FileUploader.Compact renders the row and FileUploader.Dropzone renders nothing in this variant, so the same compound layout serves every variant.
Naming the input
The hidden file input is named by the dropzone label, and the button’s text is the browse label. The demo sets them to “Attachments” and “Attach files”.