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.
Cette page a été traduite par IA et peut contenir des inexactitudes. Lire l'original en anglais
Un seul provider pour l'application
<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.
<DnDProvider> lève
No <DnDProvider> found above this component : l'inject de Vue ne voit que les ancêtres.Ce qu'il gère
| Partie | Ce que fait le provider |
|---|---|
| Entrées | Transforme les entrées au pointeur, tactiles et au clavier en glissements (sensors, activation), un glissement à la fois. |
| Recherche de la cible | Teste les collisions avec les cibles de dépôt sous le glissement (collision) et les en informe. |
| L'overlay | Rend la boîte qui suit le pointeur, téléportée dans body, et la fait atterrir au dépôt. |
| La sélection | Un seul ensemble d'ids sélectionnés, partagé par toutes les listes et tous les éléments déplaçables sous lui. |
| Défilement automatique | Fait défiler les conteneurs et la fenêtre quand un glissement approche de leurs bords. |
| La région live | Annonce 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.
| Prop | Par défaut | Ce qu'elle règle |
|---|---|---|
collision | pointerFirst | Comment la cible sous le glissement est choisie. Plus bas. |
activation | souris 4 px, toucher 200 ms | Quand un appui devient un glissement, selon le type de pointeur. Plus bas. |
autoScroll | activé | Défilement aux bords : { threshold, maxSpeed, acceleration } ou false. Plus bas. |
haptics | une courte vibration à la saisie tactile | Les 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 / dropAnimation | activée | Les items qui glissent pendant le tri / l'atterrissage de l'overlay : voir les animations. |
animateChanges | désactivée | Valeur par défaut des listes et des arbres : animer les changements de données qui ne viennent pas d'un glissement. |
keyboard | Space / Enter, Escape | Touches et pas du clavier ; false désactive les glissements au clavier. Voir l'accessibilité. |
sensors | pointeur + clavier | Ce 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. |
overlayTransition | un 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. |
overlayPreviews | 5 | Multi-drag : combien d'items glissés reçoivent un clone à la saisie, ou 'all'. Voir l'overlay par défaut. |
announcements | intégrées | Les textes pour les lecteurs d'écran, ou false. |
screenReaderInstructions | intégré | Le texte vers lequel chaque élément déplaçable pointe avec aria-describedby. |
defaultStyles | true | Injecte 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()
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();
| Champ | Ce 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). |
active | Ce 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. |
over | La cible sous le glissement : id, data, element, rect et accepted. null au-dessus de rien. |
pointer | Le pointeur en coordonnées du viewport, après les modificateurs. |
activation.progress | De 0 à 1 pendant qu'un appui attend son activation : dessinez un anneau avec. |
isAutoScrolling | Le défilement automatique fait défiler quelque chose. |
selection | La 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. |
<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énement | Quand |
|---|---|
onDragStart | Un glissement commence. |
onDragMove | Le glissement bouge. |
onDragOver | La cible sous le glissement change. |
onBeforeDrop | Le glissement est relâché. Sa réponse décide du dépôt : voir les décisions de dépôt. |
onDragEnd | Le 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. |
onDragCancel | Escape, un dépôt sur une cible qui le refuse, ou un onBeforeDrop qui a répondu 'reject'. |
onDragResume | onBeforeDrop a répondu 'continue' : rien n'a été déposé, et le glissement continue. |
onDragSettle | L'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.
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"><DnDProvider :collision="{{ mode }}"></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 :
| Fonction | Classe | Idéale pour |
|---|---|---|
pointerFirst | Ce 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. |
pointerWithin | Uniquement ce qui est sous le pointeur, la plus intérieure en premier. | Des cibles précises : un interstice ne dépose nulle part. |
rectIntersection | Chaque 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. |
closestCenter | Chaque 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. |
closestCorners | Chaque 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
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);
};
| Argument | Ce qu'il contient |
|---|---|
pointer | Le pointeur, après les modificateurs. |
overlayRect | La boîte glissée telle qu'elle est affichée, null sans overlay. |
candidates | Chaque 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). |
hits | Les 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)).
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 } }">
| Pointeur | Par 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.
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.
| Option | Par défaut | Ce qu'elle règle |
|---|---|---|
threshold | 0.2 | La zone de bord, en fraction de la taille du conteneur, entre 24 et 96 px. |
acceleration | 10 | La 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. |
maxSpeed | 25 | La 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 quand | Par défaut |
|---|---|---|
start | L'appui long devient un glissement. | true (10 ms) |
enter | Le glissement entre dans une cible qui l'accepte. | désactivé |
drop | Le dépôt aboutit. | désactivé |
refuse | Le 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.
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"><DnDProvider></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.
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
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.