Monet
@etched/monet 0.0.1

File input

Ported

A button that opens the file picker (or a box to drop files on), and the list of chosen files, each with a remove button. It is always in a Field, which shows why a file was not added. Native <button> attributes and ref pass through to the button.

Résumé
  • ana-ruiz-resume.pdf243 KB
import { useState } from "react";
import { FileInput } from "@etched/monet/file-input";

export function Preview() {
  const [files, setFiles] = useState(() => [new File([new Uint8Array(248_000)], "ana-ruiz-resume.pdf", { type: "application/pdf" })]);

  return (
    <div className="grid">
      <FileInput label="Résumé" accept=".pdf,.docx,.txt" value={files} onValueChange={setFiles}>
        Upload résumé
      </FileInput>
    </div>
  );
}

Installation

import { FileInput } from "@etched/monet/file-input";

Examples

Résumé

One file: a new pick replaces it. accept and maxSize check each pick; a file that fails is not added, and the field shows why. The app holds the list in value.

Résumé

PDF, DOCX or text, up to 10 MB.

import { useState } from "react";
import { FileInput } from "@etched/monet/file-input";

const MB = 1024 * 1024;

export function Resume() {
  const [files, setFiles] = useState<File[]>([]);

  return (
    <div className="grid">
      <FileInput label="Résumé" accept=".pdf,.docx,.txt" maxSize={10 * MB} description="PDF, DOCX or text, up to 10 MB." value={files} onValueChange={setFiles}>
        Upload résumé
      </FileInput>
    </div>
  );
}

Drop zone

dropzone adds a box to drop files on. With multiple, each pick or drop adds to the list; maxFiles caps it, and each file past the cap gets its own message.

Candidate files
Drop files here, or

Up to 3 files, 10 MB each.

import { FileInput } from "@etched/monet/file-input";

const MB = 1024 * 1024;

export function DropZone() {
  return (
    <div className="grid">
      <FileInput label="Candidate files" multiple dropzone maxFiles={3} maxSize={10 * MB} description="Up to 3 files, 10 MB each.">
        Choose files
      </FileInput>
    </div>
  );
}

In a form

With name, a native form submit sends the files; required stops a submit while no file is chosen.

Signed offer
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { FileInput } from "@etched/monet/file-input";

export function InAForm() {
  const [sent, setSent] = useState("");

  return (
    <form
      className="grid"
      onSubmit={(event) => {
        event.preventDefault();
        const files = new FormData(event.currentTarget).getAll("attachment");
        setSent(`Sent ${files.length} file${files.length === 1 ? "" : "s"}.`);
      }}
    >
      <FileInput label="Signed offer" name="attachment" required accept=".pdf">
        Choose file
      </FileInput>
      <div className="row">
        <Button type="submit" variant="primary">
          Send
        </Button>
        <span role="status">{sent}</span>
      </div>
    </form>
  );
}

Optional and disabled

Cover letter (optional)
Offer letter
Drop a file here, or
  • offer-letter-ana-ruiz.pdf178 KB
import { FileInput } from "@etched/monet/file-input";

const offer = new File([new Uint8Array(182_000)], "offer-letter-ana-ruiz.pdf", { type: "application/pdf" });

export function OptionalAndDisabled() {
  return (
    <div className="grid">
      <FileInput label="Cover letter" required={false} accept=".pdf">
        Upload cover letter
      </FileInput>
      <FileInput label="Offer letter" value={[offer]} dropzone disabled>
        Upload offer letter
      </FileInput>
    </div>
  );
}

Accessibility

  • The button and each remove button are native buttons: Enter and Space press them. The button opens the system file picker.
  • The button is named by the field's label and its own text, such as "Résumé Upload résumé". Each remove button is named "Remove" and the file name, or by removeLabel.
  • Screen readers hear "Added" with the file names, or why a file was refused, through the component's own polite live region.
  • A refusal takes the place of description in the button's aria-describedby, and the button gets aria-invalid.
  • After a remove, focus goes to the next remove button, else the previous one, else the button. An empty required submit, in a Form too, focuses the button.
  • Give it a label, so the button says what the file is for. The drop zone is for pointers only; the button is the keyboard path.

API reference

PropTypeDefaultDescription
labelReactNode–Visible label. Without one, give the control an aria-label.
labelTooltipReactNode–More about the field, behind an info button after the label.
labelTooltipLabelstring"More information"Names the info button.
descriptionReactNode–Help text below the control. Hidden while an error shows.
errorFieldError–
valueFile[]–The chosen files, when the app controls them.
defaultValueFile[][]The first files, when the input keeps its own list.
onValueChange(value: File[]) => void–Called with the new list after a pick, a drop or a remove. Files that fail a check are left out.
acceptstring–The file types allowed, as on a native input: ".pdf,.docx" or "image/*". The picker offers only these, and a file of another type is not added.
multipleboolean–Each pick adds to the list (a file already in it is not added again). Without it, a pick replaces the file.
maxSizenumber–The largest file allowed, in bytes. A larger file is not added.
maxFilesnumber–The most files the list holds, with multiple. Files past it are not added.
dropzoneboolean–A box the user can drop files on, around the button. Without it, there is only the button.
namestring–The form field name: a native form submit sends the files under it.
requiredboolean–true stops a form submit while no file is chosen; false adds the optional marker after the label.
disabledboolean–
removeLabel(file: File) => string`Remove ${file.name}`Names a file's remove button.
typeMessage(file: File) => string`${file.name} is not an accepted file type (.pdf, .docx).`Shown when a file's type is not in accept.
sizeMessage(file: File) => string`${file.name} is larger than 5 MB.`Shown when a file is larger than maxSize.
limitMessage(file: File) => string`${file.name} was not added: the limit is 3 files.`, or `…: choose one file.`Shown when a file would go past maxFiles, or past one file without multiple.
missingMessagestring"Choose a file.", or "Choose at least one file." with `multiple`Shown when a submit finds no file and the input is required.
addedLabel(files: File[]) => string`Added ${names}`What screen readers hear when files are added.
dropLabelstring"Drop a file here, or", or "Drop files here, or" with `multiple`The text before the button in a dropzone.
childrenReactNode"Choose file", or "Choose files" with `multiple`The button's text.
classNamestring–Class on the outer element, for layout.