File uploader: The tile layout
Set variant="tile" and the file uploader is a single square where the picked image replaces the drop zone. The preview shows at once, from a local object URL, while the upload runs. Use it for avatars, logos and covers.
Add a photoimage/* · up to 5 MB
import { FileUploader } from "slotsmith/file-uploader";
import { fakeUpload } from "../shared/fake-upload";
export default function Tile() {
return (
<div style={{ display: "grid", gap: "0.5rem", justifyItems: "start" }}>
<p aria-hidden="true">Profile photo</p>
{/* One square: the picked image replaces the drop zone at once, from a local preview. */}
<FileUploader
variant="tile"
accept="image/*"
maxSize={5 * 1024 * 1024}
upload={fakeUpload}
// The zone is a button; this label is its accessible name.
labels={{ dropzone: "Profile photo", title: "Add a photo" }}
/>
</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)}` };
}Pick an image: it fills the tile, a progress bar runs along its bottom edge, and the Remove button in the corner brings the empty tile back.
How the tile works
- The zone shows the newest item: its preview, its progress while it uploads, its error if it fails, and its actions. There is no list.
- Without
multiple, a new pick replaces the current image, as a profile photo should. - The preview comes from the file before the upload finishes, so it never waits for the server. After a reload, pass the stored URL as
defaultValue. - The tile is 160px square. Set the
--sfu-tile-sizetoken on the component or an ancestor to change it; see Styling.
Naming the tile
The zone is a button named by the dropzone label, “Add files” by default. The demo sets it to “Profile photo”, so a screen reader announces what the tile is for, and sets the title shown inside the empty tile.