Raccourci clavier : Ctrl + K
Commencer

Boîte de dialogue

Un <dialog> natif ouvert en modal : le piège à focus, la page inerte derrière et la couche supérieure viennent tous du navigateur. VDialogAlert est la même boîte, resserrée sur une question à laquelle il faut répondre.

Utilisation

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDialog, VTypography } from 'vectis-ui'

const open = ref(false)
</script>

<template>
  <VDialog v-model:open="open" title="Share this file" subtitle="Choose who can open it.">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps">Share</VButton>
    </template>
    <VTypography>Anyone with the link can open this file.</VTypography>
    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="open = false">Cancel</VButton>
      <VButton @click="open = false">Share</VButton>
    </template>
  </VDialog>
</template>

Exemples

Largeur

width accepte une longueur CSS dans n'importe quelle unité, 400px par défaut. La boîte ne dépasse jamais la fenêtre et garde une marge de chaque côté.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDialog, VTypography } from 'vectis-ui'

/* One dialog per width, each opened by its own button. Whatever is asked for, the
   dialog is never allowed past the viewport, margins included. */
const WIDTHS = ['320px', '480px', '40rem']

const opened = ref<string | null>(null)
</script>

<template>
  <div class="row">
    <VButton
      v-for="width in WIDTHS"
      :key="width"
      variant="outline"
      tone="neutral"
      @click="opened = width"
    >
      {{ width }}
    </VButton>
  </div>

  <VDialog
    v-for="width in WIDTHS"
    :key="width"
    :width="width"
    :open="opened === width"
    :title="`A ${width} dialog`"
    subtitle="The width is a CSS length, in any unit."
    @update:open="(value) => !value && (opened = null)"
  >
    <VTypography>
      Past the viewport the dialog stops growing and keeps a margin on either side, so a width set
      in pixels never has to be defended against a narrow screen.
    </VTypography>
    <template #footer>
      <VButton @click="opened = null">Close</VButton>
    </template>
  </VDialog>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-2);
}
</style>

Contenu long

Seul le corps défile, l'en-tête et le pied restant en place. Des filets apparaissent sous l'en-tête et au-dessus du pied tant que du contenu passe derrière eux.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDialog, VTypography } from 'vectis-ui'

const open = ref(false)

const CLAUSES = [
  'The service is provided as it stands, and the clauses below say what that means in practice. Read them once: they are short, and every one of them is there because someone asked.',
  'An account belongs to one person. Sharing credentials is what makes an audit trail useless, and an audit trail is the only thing that can tell us who changed what when something goes wrong.',
  'Content you upload stays yours. We store it, serve it back to you, and nothing else happens to it: it is not read, not indexed for anyone but you, and never used to train anything.',
  'Backups are kept for thirty days. After that a deletion is final and cannot be appealed, so export anything you may want before you remove it rather than after.',
  'Usage limits are published on the pricing page and apply per organisation rather than per seat. Going past one slows requests down; it never drops them.',
  'We announce a breaking change at least ninety days before it ships, by email to every administrator and in the changelog. A deprecation keeps working for the whole of that period.',
  'Support is answered within one working day, Monday to Friday, in English and in French. An incident affecting availability is answered whatever the day.',
  'Either side may end the agreement with thirty days of notice, in writing. Ending it does not delete anything: the export stays available for the thirty days that follow.',
  'A refund covers the unused remainder of a period, counted in whole days, and is paid back by the route the payment came in on.',
  'Availability is measured monthly and published. A month below the figure we commit to is credited without your having to ask for it.',
  'Personal data is processed in the European Union. The list of subprocessors is public, and a new one is announced thirty days before it is used.',
  'Security reports are welcome at the address on the security page. We answer within two working days and never take action against a reporter acting in good faith.',
  'An account inactive for two years is closed after three warnings sent a month apart, the last of them naming the date.',
  'The API is versioned in its path. A version stays supported for at least eighteen months after its successor ships.',
  'Rate limits are returned in the response headers rather than documented in one place and forgotten. What the headers say is what applies.',
  'You may resell what you build on the service. You may not resell the service itself as if it were yours.',
  'Trademarks stay with whoever owns them. Using our name to say what your product integrates with is fine; using it as if it were your own is not.',
  'A price change applies at the next renewal and never in the middle of a period, with sixty days of notice.',
  'Taxes are added where they are owed, worked out from the billing address you give us.',
  'An invoice disputed in writing within sixty days is looked at; past that it is taken as accepted.',
  'These terms are governed by French law, and disputes that cannot be settled between us are heard in Paris.',
  'A clause a court sets aside is removed and the rest stands, rather than the whole agreement falling with it.',
  'Nothing here removes a right the law gives you, whichever of the two says otherwise.',
  'The version that applies is the one published on the day you agreed to it, and every version is kept and dated.',
]
</script>

<template>
  <VDialog v-model:open="open" title="Terms of service" subtitle="Scroll to read all of it.">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps">Read the terms</VButton>
    </template>

    <VTypography v-for="(clause, index) in CLAUSES" :key="clause" class="clause">
      {{ index + 1 }}. {{ clause }}
    </VTypography>

    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="open = false">Decline</VButton>
      <VButton @click="open = false">Accept</VButton>
    </template>
  </VDialog>
</template>

<style scoped>
.clause {
  margin-block-end: var(--vectis-space-3);
}
</style>

En-tête personnalisé

Le slot #header remplace tout le bloc titre et sous-titre. La prop title est alors ignorée : nommez la boîte avec un aria-label. La croix de fermeture n'est pas touchée.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VAvatar, VButton, VDialog, VTypography } from 'vectis-ui'

const open = ref(false)
const following = ref(false)
</script>

<template>
  <!-- The slot replaces the title, so the dialog is named with an `aria-label` instead:
       there is no longer a `title` prop for it to point at. -->
  <VDialog v-model:open="open" width="440px" aria-label="Nadia Rousseau">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps">Open the profile</VButton>
    </template>

    <template #header>
      <div class="profile">
        <VAvatar name="Nadia Rousseau" size="lg" />
        <div class="identity">
          <VTypography as="h2" variant="heading-4">Nadia Rousseau</VTypography>
          <VTypography variant="body-sm" tone="muted">Design systems, Paris</VTypography>
        </div>
        <VButton
          size="sm"
          :variant="following ? 'outline' : 'solid'"
          :tone="following ? 'neutral' : 'accent'"
          @click="following = !following"
        >
          {{ following ? 'Following' : 'Follow' }}
        </VButton>
      </div>
    </template>

    <VTypography>
      Maintains the component library and reviews every change to the token source. Ask her about
      anything to do with theming.
    </VTypography>

    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="open = false">Close</VButton>
      <VButton @click="open = false">Send a message</VButton>
    </template>
  </VDialog>
</template>

<style scoped>
.profile {
  display: flex;
  flex: 1;
  align-items: center;
  gap: var(--vectis-space-3);
}
.identity {
  flex: 1;
  min-inline-size: 0;
}
</style>

Actions d'en-tête

Le slot #header-actions ajoute des contrôles à l'en-tête, rendus avant la croix de fermeture pour que celle-ci reste au bord.

vue
<script setup lang="ts">
import { ref } from 'vue'
import {
  VButton,
  VDialog,
  VIconButton,
  VMenu,
  VMenuItem,
  VMenuSeparator,
  VTypography,
} from 'vectis-ui'
import { info, more_horiz as moreHoriz } from 'vectis-ui/icons'

const open = ref(false)
const details = ref(false)
</script>

<template>
  <VDialog v-model:open="open" width="480px" title="Quarterly report" subtitle="report-q3.pdf">
    <template #trigger="{ triggerProps }">
      <VButton v-bind="triggerProps">Open the preview</VButton>
    </template>

    <!-- Rendered before the close cross, which keeps the cross at the edge where the
         reader looks for it. -->
    <template #header-actions>
      <VIconButton
        :icon="info"
        label="File details"
        variant="ghost"
        tone="neutral"
        size="sm"
        @click="details = !details"
      />
      <VMenu>
        <template #trigger="{ triggerProps }">
          <VIconButton
            v-bind="triggerProps"
            :icon="moreHoriz"
            label="More actions"
            variant="ghost"
            tone="neutral"
            size="sm"
          />
        </template>
        <VMenuItem label="Rename" />
        <VMenuItem label="Duplicate" />
        <VMenuSeparator />
        <VMenuItem label="Delete" tone="danger" />
      </VMenu>
    </template>

    <VTypography v-if="details" tone="muted" variant="body-sm">
      PDF, 2.4 MB, updated on 12 September.
    </VTypography>
    <VTypography>
      The report covers the third quarter and supersedes the figures circulated in August.
    </VTypography>

    <template #footer>
      <VButton @click="open = false">Close</VButton>
    </template>
  </VDialog>
</template>

Fermeture

hideClose retire la croix, persistentBackdrop ignore un clic à l'extérieur et persistentEscape ignore la touche. Fermer toutes les issues rend un pied obligatoire.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDialog, VTypography } from 'vectis-ui'

const noCross = ref(false)
const noBackdrop = ref(false)
const locked = ref(false)
</script>

<template>
  <div class="row">
    <VButton variant="outline" tone="neutral" @click="noCross = true">No cross</VButton>
    <VButton variant="outline" tone="neutral" @click="noBackdrop = true">
      Backdrop does nothing
    </VButton>
    <VButton variant="outline" tone="neutral" @click="locked = true">Footer only</VButton>
  </div>

  <!-- Escape and the backdrop still work, so the reader is never trapped. -->
  <VDialog
    v-model:open="noCross"
    hide-close
    title="Publish this version?"
    subtitle="Escape and a click outside still close it."
  >
    <VTypography>
      Taking the cross away suits a short decision, where the footer already says what the two ways
      out are.
    </VTypography>
    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="noCross = false">Cancel</VButton>
      <VButton @click="noCross = false">Publish</VButton>
    </template>
  </VDialog>

  <VDialog
    v-model:open="noBackdrop"
    persistent-backdrop
    title="Unsaved changes"
    subtitle="A click outside is ignored. Escape and the cross are not."
  >
    <VTypography>
      Worth it for a form in progress, where a stray click outside would lose what has been typed.
    </VTypography>
    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="noBackdrop = false">Discard</VButton>
      <VButton @click="noBackdrop = false">Keep editing</VButton>
    </template>
  </VDialog>

  <!-- Both refused: the footer is the only way out. Escape alone cannot be refused while
       the backdrop still closes, so the two go together. -->
  <VDialog
    v-model:open="locked"
    hide-close
    persistent-backdrop
    persistent-escape
    title="Accept the new terms"
    subtitle="One of the two buttons, and nothing else."
  >
    <VTypography>
      Nothing dismisses this by accident. Answer explicitly, and give every such dialog a footer:
      without one there is no way out at all.
    </VTypography>
    <template #footer>
      <VButton variant="ghost" tone="neutral" @click="locked = false">Read them again</VButton>
      <VButton @click="locked = false">Accept</VButton>
    </template>
  </VDialog>
</template>

<style scoped>
.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--vectis-space-2);
}
</style>

Boîte d'alerte

VDialogAlert est cette même boîte aux options figées : elle est annoncée comme une alerte, et il n'y a ni croix, ni Échap, ni clic extérieur, si bien que son pied n'est pas optionnel.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { VButton, VDialogAlert, VTypography } from 'vectis-ui'

const open = ref(false)
const deleted = ref(false)

function confirmDelete() {
  deleted.value = true
  open.value = false
}
</script>

<template>
  <div class="demo">
    <VDialogAlert
      v-model:open="open"
      title="Delete this project?"
      subtitle="Its issues, branches and releases go with it."
    >
      <template #trigger="{ triggerProps }">
        <VButton v-bind="triggerProps" tone="danger">Delete Meridian</VButton>
      </template>

      <VTypography>
        There is no cross, Escape does nothing and a click outside does nothing: the two buttons
        below are the only way out, which is why supplying them is not optional.
      </VTypography>

      <template #footer>
        <VButton variant="ghost" tone="neutral" @click="open = false">Cancel</VButton>
        <VButton tone="danger" @click="confirmDelete">Delete</VButton>
      </template>
    </VDialogAlert>

    <VTypography v-if="deleted" tone="danger" variant="body-sm">Meridian was deleted.</VTypography>
  </div>
</template>

<style scoped>
.demo {
  display: grid;
  justify-items: start;
  gap: var(--vectis-space-3);
}
</style>

API

Props

VDialog
PropTypeDéfaut
titlestringaucune
Le titre de la boîte, qui la nomme aussi pour les technologies d'assistance. Il est ignoré quand le slot #header remplace tout l'en-tête.
subtitlestringaucune
Une ligne sous le titre, qui explique ce que la boîte demande.
widthnumber | stringaucune
La largeur de la boîte : un nombre est lu en pixels, une chaîne comme n'importe quelle longueur CSS. Sans valeur, elle prend le token --vectis-control-size-dialog-width, 400px par défaut. Elle n'est jamais autorisée à dépasser la largeur de la fenêtre.
roleDialogRole'dialog' | 'alertdialog''dialog'
Le genre de boîte. alertdialog est fait pour celle à laquelle il faut répondre explicitement, et il pousse les lecteurs d'écran à l'annoncer avec plus d'insistance.
hideClosebooleanfalse
Retire la croix de fermeture de l'en-tête, ne laissant au lecteur qu'Échap, l'arrière-plan et ce que le pied propose.
persistentBackdropbooleanfalse
Empêche un clic hors de la boîte de la fermer.
persistentEscapebooleanfalse
Empêche la touche Échap de fermer la boîte. Refuser Échap alors que l'arrière-plan ferme encore ne peut pas s'exprimer nativement : les deux voies restent alors ouvertes.
closeLabelstringaucune
Ce que fait la croix de fermeture, en mots. Elle retombe sur le dictionnaire du design system.
v-model:openbooleanfalse
Si la boîte est affichée. Elle part fermée, et la liaison est bidirectionnelle : le navigateur y réécrit chaque fois que la boîte se ferme d'elle-même, par Échap ou par l'arrière-plan, si bien que vous n'avez jamais à la réinitialiser à la main.
VDialogAlert
PropTypeDéfaut
titlestringaucune
La question posée, qui nomme aussi la boîte pour les technologies d'assistance. Elle est ignorée quand le slot #header remplace tout l'en-tête.
subtitlestringaucune
Une ligne sous le titre, qui détaille les conséquences de la réponse.
widthnumber | stringaucune
La largeur de la boîte : un nombre est lu en pixels, une chaîne comme n'importe quelle longueur CSS. Sans valeur, elle prend le token --vectis-control-size-dialog-width, 400px par défaut. Elle n'est jamais autorisée à dépasser la largeur de la fenêtre.
v-model:openbooleanfalse
Si l'alerte est affichée. Elle part fermée, et la fermeture y réécrit.

Slots

VDialog
SlotType
default{}
Le corps de la boîte. C'est la partie qui défile quand il y en a trop.
header{}
Remplace le bloc titre et sous-titre par un contenu à vous.
header-actions{}
Des contrôles supplémentaires dans l'en-tête, placés avant la croix de fermeture : un menu, une bascule plein écran.
footer{}
Les boutons au pied de la boîte.
trigger{ triggerProps: DialogTriggerProps; }
Le bouton qui ouvre la boîte. Liez les triggerProps qu'il reçoit dessus. Il reste rendu en permanence, contrairement à la boîte elle-même.
VDialogAlert
SlotType
default{}
Ce que dit l'alerte.
header{}
Remplace le bloc titre et sous-titre par un contenu à vous.
header-actions{}
Des contrôles supplémentaires dans l'en-tête, là où une boîte les place avant sa croix : un lien vers l'aide, par exemple. Une alerte n'a pas de croix, ils se placent donc seuls au bout de l'en-tête.
footer{}
Les boutons qui répondent à l'alerte. Ils ne sont pas optionnels : rien d'autre ne peut fermer cette boîte.
trigger{ triggerProps: DialogTriggerProps; }
Le bouton qui ouvre l'alerte. Liez les triggerProps qu'il reçoit dessus.

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 type DialogTriggerProps = {
  onClick: () => void
  'aria-haspopup': 'dialog'
}

Variables CSS

TokenValeur
--vectis-control-size-dialog-width25rem