Documentation/Guides

Le provider

DnDProvider gère chaque glissement sous lui. Il trouve la cible, démarre les glissements, fait défiler, et informe useDnD et useDnDMonitor de tout ce qui se passe.

Modifier cette page

Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais

Un seul provider pour l'application

App.vue
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>

<template>
  <DnDProvider>
    <RouterView />
  </DnDProvider>
</template>

Placez un DnDProvider tout en haut, dans App.vue ou un layout, et n'y pensez plus. Chaque composable (makeSortableList, makeDraggable, useDnD…) cherche le provider le plus proche au-dessus du composant dans lequel il s'exécute.

Un composable doit s'exécuter dans un composant rendu à l'intérieur du provider. L'appeler dans le composant dont le propre template contient <DnDProvider> lève No <DnDProvider> found above this component : l'inject de Vue ne voit que les ancêtres.

Ce qu'il gère

PartieCe que fait le provider
EntréesTransforme les entrées au pointeur, tactiles et au clavier en glissements (sensors, activation), un glissement à la fois.
Recherche de la cibleTeste les collisions avec les cibles de dépôt sous le glissement (collision) et les en informe.
L'overlayRend la boîte qui suit le pointeur, téléportée dans body, et la fait atterrir au dépôt.
La sélectionUn seul ensemble d'ids sélectionnés, partagé par toutes les listes et tous les éléments déplaçables sous lui.
Défilement automatiqueFait défiler les conteneurs et la fenêtre quand un glissement approche de leurs bords.
La région liveAnnonce aux lecteurs d'écran les saisies, déplacements, dépôts et annulations.

Il ne rend rien d'autre : votre balisage reste tel quel.

Les options en un coup d'œil

Chaque option a une valeur par défaut qui fonctionne, le provider n'en reçoit donc généralement aucune. Le provider lit ses props au moment où il en a besoin : les démos ci-dessous changent collision, activation et autoScroll en direct.

PropPar défautCe qu'elle règle
collisionpointerFirstComment la cible sous le glissement est choisie. Plus bas.
activationsouris 4 px, toucher 200 msQuand un appui devient un glissement, selon le type de pointeur. Plus bas.
autoScrollactivéDéfilement aux bords : { threshold, maxSpeed, acceleration } ou false. Plus bas.
hapticsune courte vibration à la saisie tactileLes vibrations des glissements tactiles, ou false. Plus bas.
copyModifier'Alt'La touche qui, maintenue pendant le glissement, transforme un déplacement en copie.
modifiers[]Des règles de mouvement pour chaque glissement : voir les contraintes.
animation / dropAnimationactivéeLes items qui glissent pendant le tri / l'atterrissage de l'overlay : voir les animations.
animateChangesdésactivéeValeur par défaut des listes et des arbres : animer les changements de données qui ne viennent pas d'un glissement.
keyboardSpace / Enter, EscapeTouches et pas du clavier ; false désactive les glissements au clavier. Voir l'accessibilité.
sensorspointeur + clavierCe qui transforme les entrées en glissements : voir les capteurs.
overlayTo'body'Où l'overlay est téléporté.
overlayFit'slot'La taille de l'overlay : celle de l'emplacement où il va atterrir, ou celle de la source. Voir l'overlay.
overlayTransitionun fondu enchaînéLa façon dont l'overlay passe d'un composant à un autre en plein glissement : false, ou des props de <Transition>. Voir les cibles qui changent l'overlay.
overlayClass—Les classes du contenu de l'overlay ([data-dnd-overlay-content]), l'élément à animer d'un bloc : un soulèvement à la saisie, une remise à plat à l'atterrissage. Voir l'overlay : soulever, incliner, reposer.
overlayPreviews5Multi-drag : combien d'items glissés reçoivent un clone à la saisie, ou 'all'. Voir l'overlay par défaut.
announcementsintégréesLes textes pour les lecteurs d'écran, ou false.
screenReaderInstructionsintégréLe texte vers lequel chaque élément déplaçable pointe avec aria-describedby.
defaultStylestrueInjecte un CSS de spécificité nulle pour les états de glissement : voir le style.

La liste complète, avec toutes les formes, se trouve dans l'API de DnDProvider. Le provider émet aussi drag-start, drag-move, drag-over, drag-end, drag-cancel, drag-resume et drag-settle, les mêmes événements que ceux qu'entend useDnDMonitor.

Lire le glissement : useDnD()

Live

Chargement de la démo…

<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Tasks from './Tasks.vue';
import StatePanel from './StatePanel.vue';
</script>

<template>
  <!-- One provider: the list drags, the panel reads the drag. Neither imports the other. -->
  <DnDProvider>
    <div class="demo">
      <Tasks />
      <StatePanel />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 24px;
}
</style>

useDnD() renvoie le glissement sous forme d'état réactif, dans n'importe quel composant sous le provider :

import { useDnD } from '@vue-dnd-kit/core';

const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
ChampCe qu'il contient
status'idle', 'activating' (un appui qui attend son activation), 'dragging', 'dropping' (aucun glissement en cours, mais un overlay atterrit encore, ou un onBeforeDrop asynchrone décide), 'selecting' (un rectangle de sélection).
activeCe qui est glissé : id, ids (tous les ids glissés, en commençant par celui qui a été saisi), items, data, type, element, rect, sensor. null au repos.
overLa cible sous le glissement : id, data, element, rect et accepted. null au-dessus de rien.
pointerLe pointeur en coordonnées du viewport, après les modificateurs.
activation.progressDe 0 à 1 pendant qu'un appui attend son activation : dessinez un anneau avec.
isAutoScrollingLe défilement automatique fait défiler quelque chose.
selectionLa sélection du provider : ids, has, add, delete, toggle, set, clear. Voir le multi-drag.
cancel()Annule le glissement en cours (les items reviennent à leur place), un appui en attente ou un rectangle de sélection.
DropHint.vue
<script setup lang="ts">
import { computed } from 'vue';
import { useDnD } from '@vue-dnd-kit/core';

const { status, over } = useDnD();
const refused = computed(() => status.value === 'dragging' && over.value?.accepted === false);
</script>

<template>
  <p v-if="refused" role="status">This list doesn't take that.</p>
</template>

Dès qu'un dépôt est décidé, active, over et pointer valent null, même pendant que l'overlay atterrit encore (status vaut 'dropping'). Une UI dérivée de ces valeurs n'affiche jamais l'ancien glissement à côté des nouvelles données. Pendant qu'un onBeforeDrop asynchrone attend, l'état reste tel qu'il était au relâchement.

Le glissement suivant n'attend pas la fin d'un atterrissage : il démarre aussitôt, et status vaut 'dragging' pendant que le dernier overlay atterrit autour de lui. Un appui sur un overlay en train d'atterrir le rattrape en plein vol : le glissement reprend là où se trouve l'overlay. onDragSettle indique quand l'overlay d'un glissement est arrivé à destination.

Écouter chaque glissement : useDnDMonitor()

import { useDnDMonitor } from '@vue-dnd-kit/core';

useDnDMonitor({
  onDragStart: ({ active }) => {},
  onDragMove: ({ pointer, delta }) => {},
  onDragOver: ({ over }) => {},                  // the target changed
  onBeforeDrop: (e) => 'accept',                 // or 'reject', 'continue', some ids; may be async
  onDragEnd: ({ active, over, dropped, accepted, position }) => {},
  onDragCancel: ({ active }) => {},
  onDragResume: ({ active }) => {},              // onBeforeDrop answered 'continue'
  onDragSettle: ({ active, over }) => {},        // its overlay has landed
});

Un moniteur entend chaque glissement sous son provider, où qu'il commence : le panneau ci-dessus journalise les glissements de la liste sans l'importer. Il s'arrête quand le composant qui l'a appelé est démonté.

ÉvénementQuand
onDragStartUn glissement commence.
onDragMoveLe glissement bouge.
onDragOverLa cible sous le glissement change.
onBeforeDropLe glissement est relâché. Sa réponse décide du dépôt : voir les décisions de dépôt.
onDragEndLe dépôt a abouti. Les listes triables ont déjà exécuté leur onDrop et écrit le nouvel ordre. dropped vaut false quand le glissement s'est terminé au-dessus d'aucune cible.
onDragCancelEscape, un dépôt sur une cible qui le refuse, ou un onBeforeDrop qui a répondu 'reject'.
onDragResumeonBeforeDrop a répondu 'continue' : rien n'a été déposé, et le glissement continue.
onDragSettleL'overlay du glissement a atterri (ou est revenu à sa place, ou a été rattrapé par le glissement suivant), avec l'événement qu'a reçu onDragEnd ou onDragCancel. D'autres glissements ont pu démarrer entre-temps.

Chaque événement transporte active, over, pointer, delta (le décalage depuis le début), position (où se trouve la boîte glissée dans over, voir glisser-déposer) et, pour les items d'une liste triable, source. Les formes complètes se trouvent dans l'API des événements.

onDragEnd est l'endroit pour le travail « après le dépôt » : sauvegarde, effacement de la sélection, analytics. Le onDrop propre à une liste s'exécute avant son écriture, il voit donc encore l'ancien ordre.

Trouver la cible

Choisissez une fonction, puis faites glisser l'épingle ou la carte au-dessus des cibles. Les badges montrent comment la fonction les classe sur cette frame ; la verte est la cible.

Live

Chargement de la démo…

<script setup lang="ts">
import { computed, ref, shallowRef } from 'vue';
import {
  DnDProvider,
  closestCenter,
  closestCorners,
  pointerFirst,
  pointerWithin,
  rectIntersection,
  type CollisionDetection,
  type DnDEndEvent,
  type Key,
} from '@vue-dnd-kit/core';
import Desk from './Desk.vue';
import { magnet } from './magnet';

const DETECTORS: Record<string, CollisionDetection> = {
  pointerFirst,
  pointerWithin,
  rectIntersection,
  closestCenter,
  closestCorners,
  magnet,
};
const names = Object.keys(DETECTORS);
const mode = ref('pointerFirst');

// The ranking of the current frame, best first: a wrapper around the chosen function
// records it, so the targets can show their rank.
const ranked = shallowRef<Key[]>([]);
const collision = computed<CollisionDetection>(() => {
  const detect = DETECTORS[mode.value]!;
  return (args) => (ranked.value = detect(args));
});

const result = ref('Drag the pin or the card over the targets');
const onEnd = ({ over }: DnDEndEvent) => {
  ranked.value = [];
  result.value = over ? `Dropped on ${over.id}` : 'Dropped on nothing';
};
const onCancel = () => {
  ranked.value = [];
  result.value = 'Cancelled';
};
</script>

<template>
  <DnDProvider :collision="collision" @drag-end="onEnd" @drag-cancel="onCancel">
    <div class="lab">
      <div class="modes" role="radiogroup" aria-label="Collision detection">
        <label v-for="name in names" :key="name" :class="{ on: mode === name }">
          <input v-model="mode" type="radio" :value="name" />
          {{ name }}
        </label>
      </div>
      <code class="call">&lt;DnDProvider :collision="{{ mode }}"&gt;</code>
      <Desk :ranked="ranked" />
      <p class="result" aria-live="polite">{{ result }}</p>
    </div>
  </DnDProvider>
</template>

<style scoped>
.lab {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

.modes {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 2px;
  max-width: 100%;
  padding: 3px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.modes label {
  padding: 5px 9px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.call {
  max-width: 100%;
  padding: 6px 10px;
  overflow-x: auto;
  border-radius: 8px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  color: #42b883;
}

.result {
  margin: 0;
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.6;
}
</style>

À chaque frame d'un glissement, le provider donne à collision les cibles de dépôt en jeu, et elle renvoie leurs ids, la meilleure en premier. Cinq fonctions sont fournies avec la bibliothèque :

FonctionClasseIdéale pour
pointerFirstCe qui est sous le pointeur ; quand il n'y a rien, ce que chevauche la boîte glissée.La valeur par défaut : précise sous le pointeur, tolérante dans les interstices.
pointerWithinUniquement ce qui est sous le pointeur, la plus intérieure en premier.Des cibles précises : un interstice ne dépose nulle part.
rectIntersectionChaque cible que chevauche la boîte glissée : la priority la plus haute, puis l'imbrication la plus profonde, puis le plus grand chevauchement.De gros items au-dessus de petites cibles, quand le pointeur est au bord de l'item.
closestCenterChaque cible, selon la distance entre le centre de la boîte et le leur.Quelque chose gagne toujours, même de loin : grilles et listes d'items de même taille.
closestCornersChaque cible, selon les distances entre les quatre coins.Des colonnes côte à côte (kanban), des cibles de hauteurs très différentes.

Sous le pointeur, le test de collision respecte ce que voit l'utilisateur : parmi des cibles superposées, seule celle du dessus est touchée (front, jamais back là où elles se chevauchent), et parmi des cibles imbriquées, la plus intérieure vient en premier (subfolder, puis folder).

Votre propre fonction

magnet.ts
import type { CollisionDetection, Coordinates, Rect } from '@vue-dnd-kit/core';

/** How far the point is from the rect: 0 inside it. */
const gap = (rect: Rect, point: Coordinates) =>
  Math.hypot(
    Math.max(rect.left - point.x, 0, point.x - rect.right),
    Math.max(rect.top - point.y, 0, point.y - rect.bottom),
  );

/** What is under the pointer; in a gap, the nearest target within 40px. */
export const magnet: CollisionDetection = ({ hits, pointer, candidates }) => {
  if (hits.length) return hits;
  return candidates
    .filter((candidate) => gap(candidate.rect, pointer) <= 40)
    .sort((a, b) => gap(a.rect, pointer) - gap(b.rect, pointer))
    .map((candidate) => candidate.id);
};
ArgumentCe qu'il contient
pointerLe pointeur, après les modificateurs.
overlayRectLa boîte glissée telle qu'elle est affichée, null sans overlay.
candidatesChaque cible en jeu : id, rect (sa partie visible, rognée par les conteneurs défilants), priority, depth (0 au sommet, plus profond quand elle est imbriquée).
hitsLes cibles sous le pointeur, issues du test de collision, la meilleure en premier.

Renvoyez un tableau vide pour « aucune cible ». Les cibles désactivées, les cibles à l'intérieur des items glissés et les cibles masquées par leurs conteneurs défilants n'atteignent jamais la fonction. Une cible qui refuse le glissement reste en compétition : au-dessus d'elle, over.accepted vaut false et un dépôt est refusé (une cible à l'intérieur d'un item triable s'efface plutôt, pour que la liste se trie au-dessus d'elle). La démo enveloppe la fonction choisie pour enregistrer son classement : (args) => (ranked.value = detect(args)).

Le provider amortit les changements que demande la fonction. Une cible que le glissement vient de quitter ne peut pas regagner avant que le pointeur ait bougé de 10 px, pour que les frontières ne clignotent pas. Entre les cibles imbriquées dans un parent (les interstices d'une colonne de cartes), la dernière cible reste.

Quand un appui devient un glissement

Un appui ne glisse pas tout de suite. activation règle, pour chaque type de pointeur (mouse, touch, pen), ce qu'il attend : une distance en px, un delay en ms (avec une tolerance au-delà de laquelle l'appui abandonne, pour que la page puisse défiler), ou la première des deux.

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
PointeurPar défaut
mouse, pen{ distance: 4 } : un clic reste un clic. Un appui de souris sur une poignée (handle) démarre tout de suite, et reste un clic s'il ne bouge pas.
touch{ delay: 200, tolerance: 5 } : un appui long, pour que les listes continuent de défiler sous le doigt.

Un type que vous omettez garde sa valeur par défaut. L'activation propre à un élément déplaçable l'emporte sur celle d'une zone de glissement, qui l'emporte sur celle du provider. Pendant qu'un appui attend, useDnD().status vaut 'activating' et activation.progress va de 0 à 1 ; Escape ou useDnD().cancel() l'abandonne. Essayez les contraintes en direct, et dessinez un anneau d'appui long à partir de la progression, dans les capteurs.

Défilement automatique

Faites glisser un canal vers le haut ou le bas de la liste. Les bandes vertes sont les zones de bord : plus vous y entrez profondément, plus la liste défile vite.

Live

Chargement de la démo…

<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Channels from './Channels.vue';

const OPTIONS = {
  threshold: [0.1, 0.2, 0.4],
  maxSpeed: [10, 25, 50],
  acceleration: [4, 10, 25],
} as const;

const on = ref(true);
// The defaults: an edge zone of 20% of the container (24 to 96px), 10px a frame at the
// edge, up to 25px a frame past it.
const options = ref({ threshold: 0.2, maxSpeed: 25, acceleration: 10 });
const autoScroll = computed(() => (on.value ? options.value : false));
const call = computed(() =>
  on.value
    ? `:auto-scroll="{ threshold: ${options.value.threshold}, maxSpeed: ${options.value.maxSpeed}, acceleration: ${options.value.acceleration} }"`
    : ':auto-scroll="false"',
);
</script>

<template>
  <DnDProvider :auto-scroll="autoScroll">
    <div class="demo">
      <div class="switches">
        <label class="switch">
          <input v-model="on" type="checkbox" />
          autoScroll
        </label>
        <fieldset v-for="(values, name) in OPTIONS" :key="name" :disabled="!on">
          <legend>{{ name }}</legend>
          <label v-for="value in values" :key="value" :class="{ on: options[name] === value }">
            <input v-model="options[name]" type="radio" :value="value" />
            {{ value }}
          </label>
        </fieldset>
      </div>
      <code class="call">{{ call }}</code>
      <Channels :threshold="on ? options.threshold : 0" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
}

.switches {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px 14px;
}

fieldset {
  display: inline-flex;
  margin: 0;
  padding: 3px;
  border: 0;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

fieldset:disabled {
  opacity: 0.4;
}

legend {
  float: left;
  align-self: center;
  padding: 0 8px 0 6px;
  font: 10px/1 ui-monospace, Menlo, Consolas, monospace;
  opacity: 0.55;
}

fieldset label {
  padding: 5px 9px;
  border-radius: 6px;
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

fieldset label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

fieldset label:has(input:focus-visible),
.switch:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

fieldset input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.switch {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 5px 10px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
  font: 12px/1.2 ui-monospace, Menlo, Consolas, monospace;
  cursor: pointer;
}

.switch input {
  accent-color: #42b883;
}

.call {
  max-width: 100%;
  padding: 6px 10px;
  overflow-x: auto;
  border-radius: 8px;
  background: color-mix(in srgb, currentColor 6%, transparent);
  font: 11px/1.4 ui-monospace, Menlo, Consolas, monospace;
  white-space: nowrap;
  color: #42b883;
}
</style>

Tout élément avec overflow: auto ou scroll défile tout seul, et la fenêtre aussi. Le provider fait d'abord défiler le conteneur sous le pointeur, puis ses ancêtres, puis la fenêtre.

OptionPar défautCe qu'elle règle
threshold0.2La zone de bord, en fraction de la taille du conteneur, entre 24 et 96 px.
acceleration10La vitesse tout au bord, en px par frame. Elle croît avec le carré de la profondeur dans la zone : le départ est donc lent et précis.
maxSpeed25La vitesse maximale. Au-delà du bord (le pointeur hors du conteneur), la vitesse continue de croître jusqu'à elle.

:auto-scroll="false" le désactive. Une direction ne défile qu'une fois que le pointeur a bougé dans ce sens : un item saisi près d'un bord ne défile donc pas tout seul. Les listes, les arbres et les zones de glissement prennent leur propre autoScroll pour leur conteneur :

makeSortableList(list, { items: channels, autoScroll: { maxSpeed: 10 } });

Les glissements au clavier ne déclenchent jamais le défilement automatique : ils font défiler l'item pour le rendre visible. isAutoScrolling (de useDnD(), ou d'une zone de glissement) vaut true pendant que quelque chose défile.

Vibrations et touche de copie

Un glissement tactile vibre brièvement quand il commence. :haptics="false" le désactive ; un objet règle chaque vibration :

<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
CléVibre quandPar défaut
startL'appui long devient un glissement.true (10 ms)
enterLe glissement entre dans une cible qui l'accepte.désactivé
dropLe dépôt aboutit.désactivé
refuseLe dépôt est refusé et les items reviennent à leur place.désactivé

Chacune prend true (le motif par défaut), une durée en ms, un motif marche/arrêt, ou false. Seuls les glissements commencés au toucher vibrent, et Safari sur iOS ne peut pas vibrer du tout.

copyModifier est la touche qui, maintenue pendant un glissement entre des listes, transforme le déplacement en copie : 'Alt' par défaut, n'importe quelle KeyboardEvent.key, ou null pour aucune. La liste d'où viennent les items a besoin d'une fonction clone ; voir les listes qui partagent des items.

Plusieurs providers

La plupart des applications en ont besoin d'un seul. Placez la liste de souhaits sous son propre provider, puis essayez d'y déplacer un livre : il ne peut pas quitter son provider, et le moniteur extérieur n'entend plus la liste de souhaits.

Live

Chargement de la démo…

<script setup lang="ts">
import { ref } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import Shelf from './Shelf.vue';
import EventLog from './EventLog.vue';

const reading = ref([
  { id: 'dune', title: 'Dune', hue: 32 },
  { id: 'emma', title: 'Emma', hue: 330 },
  { id: 'ulysses', title: 'Ulysses', hue: 210 },
]);
const wishlist = ref([
  { id: 'beloved', title: 'Beloved', hue: 140 },
  { id: 'solaris', title: 'Solaris', hue: 260 },
]);

const separate = ref(false);
</script>

<template>
  <DnDProvider>
    <div class="demo">
      <div class="modes" role="radiogroup" aria-label="Wishlist">
        <label :class="{ on: !separate }"><input v-model="separate" type="radio" :value="false" />one provider</label>
        <label :class="{ on: separate }"><input v-model="separate" type="radio" :value="true" />wishlist in its own</label>
      </div>
      <div class="columns">
        <div class="column">
          <Shelf id="reading" v-model="reading" title="Reading" />
          <EventLog title="outer monitor" />
        </div>
        <!-- A provider inside a provider: its items belong to it, and drags never cross. -->
        <DnDProvider v-if="separate">
          <div class="column island">
            <span class="tag">&lt;DnDProvider&gt;</span>
            <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
            <EventLog title="inner monitor" />
          </div>
        </DnDProvider>
        <div v-else class="column">
          <Shelf id="wishlist" v-model="wishlist" title="Wishlist" />
        </div>
      </div>
    </div>
  </DnDProvider>
</template>

<style scoped>
.demo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 18px;
}

.modes {
  display: inline-flex;
  padding: 3px;
  border-radius: 9px;
  background: color-mix(in srgb, currentColor 7%, transparent);
}

.modes label {
  padding: 5px 10px;
  border-radius: 6px;
  font-size: 12px;
  cursor: pointer;
}

.modes label.on {
  background: Canvas;
  box-shadow: 0 1px 3px rgb(0 0 0 / 0.25);
  font-weight: 600;
}

.modes label:has(input:focus-visible) {
  outline: 2px solid #42b883;
}

.modes input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.columns {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 230px));
  gap: 16px;
  max-width: 100%;
}

.column {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
}

.island {
  position: relative;
  margin: -10px;
  padding: 10px;
  border: 1.5px dashed color-mix(in srgb, #42b883 60%, transparent);
  border-radius: 16px;
}

.tag {
  position: absolute;
  top: -9px;
  right: 14px;
  padding: 0 6px;
  background: Canvas;
  font: 10px/18px ui-monospace, Menlo, Consolas, monospace;
  color: #42b883;
}
</style>
<DnDProvider>
  <Shelf v-model="reading" />
  <DnDProvider>
    <Shelf v-model="wishlist" />
    <EventLog />            <!-- hears the inner provider's drags only -->
  </DnDProvider>
</DnDProvider>
  • Les providers sont indépendants : un glissement ne passe jamais de l'un à l'autre.
  • Les providers imbriqués sont autorisés. Le propriétaire le plus proche l'emporte : un item appartient au provider le plus proche au-dessus de lui, et le provider extérieur l'ignore.
  • Chaque provider a sa propre sélection, son propre overlay, sa propre région live et ses propres moniteurs.

Un second provider n'a de sens que pour une partie de la page qui a besoin de ses propres réglages à l'échelle du provider (collision, touches du clavier, annonces) et dont les items n'en sortent jamais. Si des items ne peuvent pas passer d'une partie de votre page à une autre, vérifiez qu'ils ne se trouvent pas sous deux providers.

Des règles pour une partie de la page (un tableau qui s'aligne sur une grille, un fil avec un appui tactile plus long, un panneau où rien ne glisse) ne nécessitent pas leur propre provider :
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
Ses items restent dans le provider de l'application et peuvent toujours aller n'importe où. Voir les contraintes.

Une application montée ailleurs, dans un shadow root ou comme application Vue séparée, peut rejoindre un provider existant : passez useDnDContext() à provideDnDContext(app, context) avant de la monter. Le test de collision et l'overlay fonctionnent à travers les shadow roots.

SSR et Nuxt

Le provider est compatible SSR. Les listes et les items sont rendus sur le serveur comme n'importe quel autre balisage. L'overlay et la région live sont téléportés et se montent uniquement côté client : ils ne provoquent donc jamais d'erreur d'hydratation. Les ids par défaut viennent du useId() de Vue et sont identiques sur le serveur et le client.

Dans Nuxt, le module enregistre DnDProvider globalement et importe automatiquement les composables : voir l'installation.