Raccourci clavier : Ctrl + K
Commencer

Sélecteur de fichiers

Le frère en zone de dépôt de VFileInput : une surface plutôt qu'un champ, avec les mêmes règles de filtrage et la même liste de fichiers en valeur.

Utilisation

vue
Drag your files herePDF or PNG, up to 5 MBou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker } from 'vectis-ui'

const files = ref<File[]>([])
</script>

<template>
  <VFilePicker v-model="files" title="Drag your files here" subtitle="PDF or PNG, up to 5 MB" />
</template>

Exemples

Titre et sous-titre

title est obligatoire et subtitle est l'endroit où écrire les règles en clair. Les deux ont un slot, qui n'accepte que du texte et des éléments en ligne.

vue
Drop your files hereou
Drop your files hereImages or PDF, up to 5 MB eachou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker } from 'vectis-ui'

const plain = ref<File[]>([])
const described = ref<File[]>([])
</script>

<template>
  <div class="column">
    <VFilePicker v-model="plain" title="Drop your files here" />

    <VFilePicker
      v-model="described"
      multiple
      accept="image/*,.pdf"
      :max-size="5_000_000"
      title="Drop your files here"
      subtitle="Images or PDF, up to 5 MB each"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

La liste des fichiers

preview dit où va la liste des fichiers choisis, sous la zone ou à côté, ou la retire. À côté, elle repasse dessous dès que le composant est étroit.

vue
Listed under the zoneWhat a narrow form wants, there being no room beside itou
Listed beside the zoneNarrow the window and it folds back underneathou
Not listed, the defaultTwo files are held all the same: the value never depends on this propou
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VFilePicker } from 'vectis-ui'

const under = ref<File[]>([])
const beside = ref<File[]>([])
const none = ref<File[]>([])

/* The same two files in all three, so the only thing that differs is where they are
   listed. They are built here rather than at setup: `File` is a browser type, and this
   page is rendered on the server before it ever reaches one. */
onMounted(() => {
  const seed = () => [
    new File([new Uint8Array(320_000)], 'contract.pdf', { type: 'application/pdf' }),
    new File([new Uint8Array(48_000)], 'figures.csv', { type: 'text/csv' }),
  ]
  under.value = seed()
  beside.value = seed()
  none.value = seed()
})
</script>

<template>
  <div class="column">
    <div class="narrow">
      <VFilePicker
        v-model="under"
        multiple
        preview="bottom"
        title="Listed under the zone"
        subtitle="What a narrow form wants, there being no room beside it"
      />
    </div>

    <!-- Left at the card's full width on purpose: the side list needs the room, and it
         folds back underneath as soon as the component itself is narrower than 34rem. -->
    <VFilePicker
      v-model="beside"
      multiple
      preview="end"
      title="Listed beside the zone"
      subtitle="Narrow the window and it folds back underneath"
    />

    <div class="narrow">
      <VFilePicker
        v-model="none"
        multiple
        title="Not listed, the default"
        subtitle="Two files are held all the same: the value never depends on this prop"
      />
    </div>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
}
.narrow {
  max-inline-size: 26rem;
}
</style>

Icônes personnalisées

icon est le grand glyphe en haut de la zone. typeIcons remplace le glyphe affiché par une ligne pour un type de fichier, en ne nommant que les types à changer, et removeIcon celui du bouton qui retire une ligne.

vue
Drop your sources hereou
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VFilePicker } from 'vectis-ui'
import { attach_file as attachFile, description } from 'vectis-ui/icons'

const files = ref<File[]>([])

/* The files are built here and not at setup: `File` is a browser type, and this page is
   rendered on the server before it ever reaches one. */
onMounted(() => {
  files.value = [
    new File([new Uint8Array(24_000)], 'styles.css', { type: 'text/css' }),
    new File([new Uint8Array(180_000)], 'assets.zip', { type: 'application/zip' }),
  ]
})
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="files"
      multiple
      preview="bottom"
      title="Drop your sources here"
      :icon="attachFile"
      :type-icons="{ code: description }"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

Vignettes

Une image est affichée telle quelle, par une adresse temporaire créée dans la page. hideThumbnails affiche à la place l'icône de son type.

vue
Thumbnails, the defaultAn image is shown as itselfou
hideThumbnailsEvery file shows its kind insteadou
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VFilePicker } from 'vectis-ui'

const shown = ref<File[]>([])
const hidden = ref<File[]>([])

/* A real PNG, drawn in the browser: a thumbnail is only ever painted from bytes an
   <img> can decode, so an empty buffer would fall back to the kind icon and prove
   nothing. Both zones are given the same two files. */
async function gradientPng(name: string): Promise<File> {
  const canvas = document.createElement('canvas')
  canvas.width = 96
  canvas.height = 96
  const context = canvas.getContext('2d')
  if (!context) return new File([], name, { type: 'image/png' })
  const gradient = context.createLinearGradient(0, 0, 96, 96)
  gradient.addColorStop(0, '#6366f1')
  gradient.addColorStop(1, '#ec4899')
  context.fillStyle = gradient
  context.fillRect(0, 0, 96, 96)
  const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'))
  return new File(blob ? [blob] : [], name, { type: 'image/png' })
}

onMounted(async () => {
  const files = [
    await gradientPng('cover.png'),
    new File([new Uint8Array(90_000)], 'notes.pdf', { type: 'application/pdf' }),
  ]
  shown.value = files
  hidden.value = [...files]
})
</script>

<template>
  <div class="grid">
    <VFilePicker
      v-model="shown"
      multiple
      preview="bottom"
      title="Thumbnails, the default"
      subtitle="An image is shown as itself"
    />

    <VFilePicker
      v-model="hidden"
      multiple
      hide-thumbnails
      preview="bottom"
      title="hideThumbnails"
      subtitle="Every file shows its kind instead"
    />
  </div>
</template>

<style scoped>
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--vectis-space-5);
}
</style>

Fichiers multiples

multiple permet à la zone de prendre plusieurs fichiers, les suivants étant écartés sinon. La valeur est une liste dans les deux cas, et reject est émis une fois par fichier refusé.

vue
One fileDrop two and the second is refusedou
As many as you likemultiple, with nothing else to bound itou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker, type FileRejection } from 'vectis-ui'

const one = ref<File[]>([])
const several = ref<File[]>([])

/* The component turns a file away and says so; showing why is the consumer's job, here
   and for every other limit. */
const refused = ref('')

function onReject({ file }: FileRejection) {
  refused.value = `${file.name} was turned away: this zone takes one file.`
}
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="one"
      preview="bottom"
      title="One file"
      subtitle="Drop two and the second is refused"
      @reject="onReject"
    />
    <p v-if="refused" class="value">{{ refused }}</p>

    <VFilePicker
      v-model="several"
      multiple
      preview="bottom"
      title="As many as you like"
      subtitle="multiple, with nothing else to bound it"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
.value {
  margin: 0;
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

Types acceptés

accept accepte la syntaxe du navigateur et filtre la boîte de dialogue système comme un fichier déposé. Un fichier qui échoue revient par reject avec la raison type.

vue
Images and PDFBrowse and the dialog offers nothing else; drop and the rule is applied againou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker, type FileRejection } from 'vectis-ui'

const files = ref<File[]>([])
const refused = ref('')

function onReject({ file }: FileRejection) {
  refused.value = `${file.name} was turned away: images and PDF only.`
}
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="files"
      multiple
      accept="image/*,.pdf"
      preview="bottom"
      title="Images and PDF"
      subtitle="Browse and the dialog offers nothing else; drop and the rule is applied again"
      @reject="onReject"
    />
    <p v-if="refused" class="value">{{ refused }}</p>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
.value {
  margin: 0;
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

Taille maximale

maxSize est la taille maximale d'un fichier, en octets. Chacun est pesé séparément.

vue
Up to 500 kB per fileEach file is weighed on its own, so a small one still gets throughou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker, type FileRejection } from 'vectis-ui'

const files = ref<File[]>([])
const refused = ref('')

function onReject({ file }: FileRejection) {
  refused.value = `${file.name} was turned away: 500 kB at most per file.`
}
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="files"
      multiple
      :max-size="500_000"
      preview="bottom"
      title="Up to 500 kB per file"
      subtitle="Each file is weighed on its own, so a small one still gets through"
      @reject="onReject"
    />
    <p v-if="refused" class="value">{{ refused }}</p>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
.value {
  margin: 0;
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

Taille totale et nombre

maxTotalSize et maxFiles bornent la sélection dans son ensemble, en comptant ce qui est déjà dans la liste. Le filtrage suit un ordre fixe : type, puis taille, puis nombre, puis taille totale.

vue
1 MB in all, three files at mostWhat is already in the list counts towards the totalou
<script setup lang="ts">
import { ref } from 'vue'
import { VFilePicker, type FileRejection } from 'vectis-ui'

const files = ref<File[]>([])
const refused = ref('')

/* The reason says which rule was met, and the two here are not the same failure: one
   batch too heavy, or one file too many. */
function onReject({ file, reason }: FileRejection) {
  refused.value =
    reason === 'count'
      ? `${file.name} was turned away: three files at most.`
      : `${file.name} was turned away: 1 MB for the whole selection.`
}
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="files"
      multiple
      :max-total-size="1_000_000"
      :max-files="3"
      preview="bottom"
      title="1 MB in all, three files at most"
      subtitle="What is already in the list counts towards the total"
      @reject="onReject"
    />
    <p v-if="refused" class="value">{{ refused }}</p>
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
.value {
  margin: 0;
  font-size: var(--vectis-text-body-sm-size);
  color: var(--vectis-color-danger-text);
}
</style>

États

readonly montre ce qui a été pris sans rien laisser changer, boutons de retrait compris. disabled grise la zone et l'empêche d'accepter quoi que ce soit, y compris en cours de glisser.

vue
Read-onlyWhat was taken stays on show, its remove buttons disabled along with the zoneou
DisabledNo dialog, no drop, and the zone greys through the colour tokensou
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { VFilePicker } from 'vectis-ui'

const taken = ref<File[]>([])
const empty = ref<File[]>([])

onMounted(() => {
  taken.value = [
    new File([new Uint8Array(320_000)], 'contract.pdf', { type: 'application/pdf' }),
    new File([new Uint8Array(48_000)], 'figures.csv', { type: 'text/csv' }),
  ]
})
</script>

<template>
  <div class="column">
    <VFilePicker
      v-model="taken"
      multiple
      readonly
      preview="bottom"
      title="Read-only"
      subtitle="What was taken stays on show, its remove buttons disabled along with the zone"
    />

    <VFilePicker
      v-model="empty"
      disabled
      title="Disabled"
      subtitle="No dialog, no drop, and the zone greys through the colour tokens"
    />
  </div>
</template>

<style scoped>
.column {
  display: flex;
  flex-direction: column;
  gap: var(--vectis-space-5);
  max-inline-size: 26rem;
}
</style>

API

Props

PropTypeDéfaut
titlestringaucune
Ce qu'on demande au lecteur de déposer, en une ligne. C'est OBLIGATOIRE : une zone de dépôt sans consigne n'est qu'un rectangle. Elle masque l'attribut HTML du même nom, compromis accepté.
subtitlestringaucune
Une seconde ligne dessous, pour les contraintes en clair : genres, tailles, nombre.
iconIconSourcecloud_upload
La grande icône en haut de la zone.
hideBrowsebooleanfalse
Masque le séparateur et le bouton de parcours sous la consigne. Cela change la NATURE de la zone : elle devient alors le contrôle elle-même, un vrai bouton, si bien qu'Entrée, Espace et le focus viennent de la plateforme plutôt que d'un conteneur qui se contente de réagir aux clics.
browseTextstringaucune
Le texte affiché sur le bouton de parcours, qui lui sert aussi de nom accessible. Il retombe sur le dictionnaire du design system.
previewFilePickerPreviewfalse | 'bottom' | 'end'false
Où les fichiers pris sont listés : sous la zone, ou à côté, auquel cas la liste repasse dessous quand le COMPOSANT est étroit, en suivant la largeur qu'on lui a donnée et non celle de la fenêtre. Par défaut, rien n'est listé du tout.
hideThumbnailsbooleanfalse
Affiche l'icône de genre pour chaque fichier de cette liste, images comprises : la porte de sortie quand une liste contient beaucoup d'images, ou de très grandes. Sans lui, une image est montrée en vignette : elle reçoit une adresse temporaire, créée dans le navigateur seulement et libérée dès que le fichier quitte la liste ou que le composant disparaît.
typeIconsPartial<Record<FilePickerKind, IconSource>>aucune
Remplace l'icône d'un ou plusieurs genres de fichiers.
removeIconIconSourceclose
L'icône du bouton qui retire un fichier de la liste.
multiplebooleanfalse
Permet de prendre plusieurs fichiers. Avec un seul, tout fichier supplémentaire est écarté.
acceptstringaucune
Quels genres de fichiers sont acceptés, dans la syntaxe du navigateur. C'est appliqué deux fois : en attribut, ce qui filtre la boîte de dialogue du système, et de nouveau en code, seule chose capable de filtrer un fichier déposé.
maxSizenumberaucune
La taille maximale d'UN fichier, en octets.
maxTotalSizenumberaucune
La taille maximale de toute la sélection, en octets.
maxFilesnumberaucune
Combien de fichiers peuvent être pris au plus.
disabledbooleanfalse
Rend la zone inutilisable, grisée par les tokens de couleur.
readonlybooleanfalse
Montre ce qui a été pris sans permettre de le changer : ni boîte de dialogue, ni dépôt, ni retrait. Ses boutons restent atteignables au clavier, annoncés comme indisponibles.
invalidbooleanfalse
Marque la zone comme invalide, ce qui colore son contour et s'annonce sur le contrôle que l'on atteint. C'est pour une règle à vous : rien ici n'est vérifié par le navigateur, le vrai champ étant masqué.
loadingbooleanfalse
Affiche un indicateur à la place de l'icône de la zone, typiquement pendant un envoi. Il dit que quelque chose se passe et ne change rien d'autre : les fichiers peuvent toujours être déposés et la boîte de dialogue s'ouvre encore.
loadingTextstringaucune
Ce que les lecteurs d'écran annoncent pendant que l'indicateur tourne. Sa valeur par défaut vient du dictionnaire du design system.
v-modelFile[][]
Toujours une LISTE de fichiers, que plusieurs soient permis ou non, jamais un fichier seul. La forme ne dépend pas d'une prop : vous n'avez donc jamais à restreindre une union que TypeScript ne sait pas discriminer.

Événements

ÉvénementType
change[files: File[]]
La sélection a changé, avec toute la liste telle qu'elle est désormais.
reject[rejection: FileRejection]
Un fichier a été écarté, avec lequel et pourquoi.
remove[file: File, index: number]
Un fichier a été retiré de la liste, avec lequel et où il se trouvait.

Slots

SlotType
icon{}
La grande icône, pour une illustration que la prop ne peut pas exprimer. Elle doit rester non interactive, et les deux suivantes aussi : avec le bouton de parcours masqué, la zone EST un bouton, et rien d'interactif ne peut se trouver dans un bouton.
title{}
La consigne. Texte et éléments en ligne seulement, pour la même raison.
subtitle{}
La seconde ligne. Même contrat que la consigne.
browseFilePickerBrowseSlotProps
Le bouton de parcours. Appelez le open qu'il reçoit : sans lui, un bouton à vous ne pourrait plus ouvrir la boîte de dialogue du tout.
itemFilePickerRow
TOUTE une ligne de la liste, la porte de sortie pour une ligne montrant sa propre progression d'envoi. Elle reçoit tout ce qu'avait la ligne standard.
thumbnailFilePickerRow
Le carré en début de ligne seulement : pour une vignette produite par votre serveur, l'image d'accroche d'une vidéo, ou un format que le navigateur ne sait pas décoder.
removeFilePickerRemoveSlotProps
Le contrôle qui retire une ligne. remove est la seule chose qui peut sortir le fichier, et removeLabel est le nom accessible tout prêt, celui du fichier compris, sans lequel le bouton ne serait annoncé comme rien du tout.

Types

Les types que les tables ci-dessus nomment, écrits comme la librairie les déclare. Ceux qui portent export s'importent depuis vectis-ui pour typer votre propre code ; les autres décrivent la forme de ce qu'un slot fournit.

export interface BuiltinIcon {
  name: string
  paths: readonly [string] | readonly [string, string]
}
export interface FilePickerBrowseSlotProps {
  open: () => void
  disabled: boolean
}
export type FilePickerKind =
  'image' | 'pdf' | 'audio' | 'video' | 'archive' | 'spreadsheet' | 'code' | 'file'
export interface FilePickerRemoveSlotProps {
  file: File
  index: number
  remove: () => void
  removeLabel: string
}
export interface FilePickerRow {
  file: File
  index: number
  kind: FilePickerKind
  thumbnail: string | undefined
  icon: IconSource
  sizeText: string
  remove: () => void
}
export type FileRejectReason = 'type' | 'size' | 'count' | 'total-size'
export interface FileRejection {
  file: File
  reason: FileRejectReason
}
export type IconRender =
  | { path: string; viewBox?: string }
  | { component: Component; props?: Record<string, unknown> }
  | { src: string }
  | { text: string; class?: string }
  | { class: string }
export type IconSource = string | BuiltinIcon | IconRender

Variables CSS

TokenValeur
--vectis-control-size-file-picker-min-block10rem
--vectis-control-size-file-picker-icon2.5rem
--vectis-control-size-file-picker-thumb2.5rem