Documentation/Guides

Capteurs et glissements programmatiques

Comment une entrée devient un glissement — les capteurs pointeur et clavier, les contraintes d'activation, canDrag, vos propres capteurs, et les glissements que vous scriptez avec @vue-dnd-kit/actions.

Modifier cette page

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

Un capteur transforme des entrées en glissements. Il dit seulement au provider « fais glisser cet item depuis ici », puis où l'entrée s'est déplacée, et si elle a déposé ou annulé. Le provider fait le reste, de la même façon pour chaque capteur : un glissement à la fois, refus des items désactivés, contraintes d'activation, tri, défilement automatique, annonces, Escape et tout le dépôt.

Les capteurs intégrés

CapteurEntrée
pointerSensorSouris, toucher et stylet, via les Pointer Events. Il capture le pointeur, empêche la page de sélectionner du texte ou de défiler sous un glissement tactile, et avale le clic qui suit un glissement.
keyboardSensorSpace et Enter saisissent et déposent, les flèches déplacent, Escape annule — n'importe quel glissement, y compris au pointeur.

Les deux tournent par défaut. Les sensors du provider remplacent la liste :

<DnDProvider :sensors="[pointerSensor, keyboardSensor, joystick.sensor]">

Pour désactiver les glissements au clavier, préférez :keyboard="false" au retrait de keyboardSensor : le capteur clavier est aussi ce qui annule n'importe quel glissement avec Escape. Ses touches se réassignent avec keyboard.keys (voir l'accessibilité).

Contraintes d'activation

Un appui ne glisse pas tout de suite : il attend sa contrainte d'activation, pour qu'un clic reste un clic et qu'un balayage puisse toujours faire défiler la page. Changez les règles de cette liste et appuyez sur une ligne :

Live

Chargement de la démo…

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

<template>
  <DnDProvider>
    <Inbox />
  </DnDProvider>
</template>

Une contrainte a trois champs :

ChampLe glissement démarre…
distanceUne fois que le pointeur s'est déplacé de ce nombre de px.
delayUne fois que l'appui a duré ce nombre de ms. Avec distance aussi, au premier des deux.
toleranceAvec delay : jusqu'où le pointeur peut bouger entre-temps. Au-delà, l'appui abandonne, pour que la page puisse défiler. 5 px par défaut quand il n'y a pas de distance.

Sans contrainte à vous, chaque type de pointeur a la sienne par défaut :

PointeurPar défaut
SourisAprès un déplacement de 4 px. Tout de suite quand l'appui est sur une poignée (handle), sauf si une contrainte souris est définie.
StyletAprès un déplacement de 4 px.
ToucherAprès un appui de 200 ms. Bouger de plus de 5 px avant cela abandonne, et la page défile à la place.

Où les définir

Par type de pointeur — mouse, touch, pen — sur le provider, une zone de glissement ou l'item. Pour chaque type, celle de l'item l'emporte, puis celle de sa zone, puis celle du provider :

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 250, tolerance: 5 } }">
// Inbox.vue: the rules of every drag that starts inside the panel.
makeDragArea(panel, { activation: () => MODES[mode.value].activation });

// One item: a longer press by touch, the area's rule for the mouse.
makeSortableItem(el, { item: () => props.task, activation: { touch: { delay: 400, tolerance: 8 } } });

Les options sont lues quand un appui commence : un getter ou une ref les change donc en direct, comme le fait la démo. Les glissements au clavier démarrent tout de suite. Votre propre capteur demande sa propre contrainte, ou celle configurée pour un type de pointeur.

Un anneau d'appui long

Pendant qu'un appui attend, useDnD().status vaut 'activating' et activation.progress va de 0 à 1 — selon le temps avec delay, selon la distance parcourue avec distance. La démo dessine un anneau là où l'appui a commencé :

Inbox.vue
<script setup lang="ts">
const { status, activation } = useDnD();
</script>

<template>
  <svg v-if="status === 'activating'" class="ring" viewBox="0 0 40 40">
    <circle cx="20" cy="20" r="17" pathLength="1" :stroke-dasharray="`${activation.progress.value} 1`" />
  </svg>
</template>

Donnez à l'anneau pointer-events: none : il se trouve sous le pointeur, et le glissement teste les collisions avec ce qui s'y trouve. Un appui qui abandonne — déplacé au-delà de sa tolerance, ou relâché trop tôt — ramène status directement à 'idle'.

Les poignées démarrent tout de suite

Avec une handle, seule la poignée démarre un glissement, et un appui de souris dessus glisse tout de suite : saisir une poignée ne laisse rien à distinguer. Un appui relâché là où il a commencé reste un clic : la poignée peut donc ouvrir un menu sur click (le pointeur n'est capturé qu'une fois qu'il bouge). Un toucher attend toujours son appui long, sauf si la poignée ne peut pas faire défiler la page. Donnez-lui touch-action: none et une courte distance à l'item :

Message.vue
makeSortableItem(el, {
  item: () => props.chat,
  handle: '.grip',
  // The grip has `touch-action: none`: a touch on it can't scroll, so it drags after 4 px.
  activation: { touch: { distance: 4 } },
});

Une contrainte souris définie sur l'item, sa zone ou le provider s'applique aussi aux poignées. Faites de la poignée un <button>, comme le fait la démo : elle prend le focus, et un glissement au clavier démarre donc aussi depuis elle.

Seulement certains appuis

canDrag est un filtre de plus sur les éléments déplaçables et les items triables. Il reçoit l'événement d'entrée et répond true ou false pour cet appui. Ici, les glissements au pointeur demandent Shift ; le clavier glisse comme d'habitude :

Live

Chargement de la démo…

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

<template>
  <DnDProvider>
    <Checklist />
  </DnDProvider>
</template>
Task.vue
makeSortableItem(el, {
  item: () => props.task,
  // Never: the row is locked (and says so to screen readers with aria-disabled).
  disabled: () => !!props.task.locked,
  // Per press: pointer drags only with Shift held; the keyboard as usual.
  canDrag: (event) => {
    if (!props.shiftOnly || event instanceof KeyboardEvent || event.shiftKey) return true;
    emit('refused');
    return false;
  },
});
OptionInterrogéeUtilisez-la pour
disabledEn direct, et posée en aria-disabledLes items qui ne peuvent pas bouger du tout : verrouillés, en lecture seule.
canDragUne fois par appui, avec son événementLes règles qui portent sur l'entrée : une touche maintenue, un bouton, un type de pointeur.
handleLà où l'appui se poseLes items qui ne démarrent un glissement que depuis une partie.

canDrag est interrogé avant la contrainte d'activation, avec le pointerdown ou le keydown. Un type de pointeur est une règle de plus qu'il peut poser :

// Mouse and pen drag; a touch on the row always scrolls the page.
canDrag: (event) => !(event instanceof PointerEvent && event.pointerType === 'touch'),

Les enfants interactifs — boutons, champs, cases à cocher, champs éditables — ne démarrent jamais de glissement au pointeur : ils continuent donc de fonctionner à l'intérieur d'un élément déplaçable, comme la case à cocher et le menu du responsable dans la démo. Avec une handle, seule la poignée en démarre un.

Votre propre capteur

Une manette, un joystick à l'écran, des commandes vocales : tout ce qui peut désigner un item et bouger peut piloter un glissement. Cette manette est un capteur. Maintenez A au-dessus d'un héros pour le saisir, dirigez avec la croix, A de nouveau le dépose, B annule. La souris et le clavier fonctionnent toujours à côté :

Live

Chargement de la démo…

<script setup lang="ts">
import { onMounted, useTemplateRef } from 'vue';
import { DnDProvider, keyboardSensor, pointerSensor } from '@vue-dnd-kit/core';
import Party from './Party.vue';
import Gamepad from './Gamepad.vue';
import { createJoystick } from './joystick';

const stage = useTemplateRef<HTMLElement>('stage');
const joystick = createJoystick(() => stage.value);
// The gamepad drives drags next to the mouse, touch and the keyboard.
const sensors = [pointerSensor, keyboardSensor, joystick.sensor];

onMounted(() => {
  const first = stage.value?.querySelector('li');
  if (first) joystick.aim(first);
});
</script>

<template>
  <DnDProvider :sensors="sensors">
    <div ref="stage" class="arcade">
      <Party />
      <Gamepad :joystick="joystick" />
      <span class="cursor" :style="{ left: `${joystick.cursor.value.x}px`, top: `${joystick.cursor.value.y}px` }" />
    </div>
  </DnDProvider>
</template>

<style scoped>
.arcade {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: 32px 48px;
  padding: 8px;
}

/* The gamepad's cursor: above everything, never what a hit test finds. */
.cursor {
  position: absolute;
  z-index: 1;
  width: 18px;
  height: 18px;
  margin: -9px 0 0 -9px;
  border: 2px solid #42b883;
  border-radius: 50%;
  background: color-mix(in srgb, #42b883 25%, transparent);
  box-shadow: 0 0 0 4px color-mix(in srgb, #42b883 15%, transparent);
  pointer-events: none;
}
</style>

defineSensor donne au capteur son nom et un setup qui reçoit le côté du provider, une fois par provider :

joystick.ts
const sensor = defineSensor({
  name: 'joystick',
  // Once per provider, in the browser. The cleanup runs when the provider unmounts.
  setup(ctx) {
    context = ctx;
    return () => {
      context = null;
      drag = null;
    };
  },
});

Un appui demande un glissement au provider avec activate(). La cible est vérifiée comme pour un vrai appui : ce doit être un élément déplaçable de ce provider, non désactivé, et pas un bouton à l'intérieur de l'un d'eux :

joystick.ts
/** A goes down: drops a running drag, or starts a hold over what is under the cursor. */
const press = () => {
  if (drag?.state === 'dragging') return void drag.drop();
  const at = point();
  const target = document.elementFromPoint(at.x, at.y);
  if (!target || !context) return;
  drag = context.activate({
    target, // checked like a real press: a draggable of this provider, not disabled
    point: at,
    type: 'pointer', // follows points: lists sort under it, containers auto-scroll
    activation: { delay: 300 }, // `useDnD().activation.progress` runs meanwhile
    onEnd: (ended) => {
      if (drag === ended) drag = null;
    },
  }); // null: nothing to drag there
};

Ensuite, le glissement bouge avec la croix, et A ou B le terminent :

joystick.ts
const move = (dx: number, dy: number) => {
  cursor.value = { x: cursor.value.x + dx, y: cursor.value.y + dy }; // clamped in the demo
  drag?.move(point()); // pending: checks the tolerance; dragging: moves
};

const release = () => {
  if (drag?.state === 'pending') drag.cancel(); // A let go before the hold is over
};

const cancel = () => drag?.cancel();

Ajoutez le capteur aux sensors du provider, comme le fait la démo, ou faites-le tourner tant qu'un composant existe avec useSensor(joystick.sensor). Les points sont en coordonnées du viewport : la démo garde son curseur dans les coordonnées de la scène, pour qu'il défile avec la page, et le convertit dans point().

Ce que reçoit le capteur

setup(context) reçoit le côté du provider :

MembreCe que c'est
propsLes props du provider (activation, keyboard…). Lisez-les au moment où vous en avez besoin : elles peuvent changer.
statusLe statut de glissement du provider, quel que soit le capteur qui mène le glissement.
dragLe glissement en cours du provider, en attente ou actif, quel que soit le capteur qui l'a démarré. null quand il n'y en a pas.
activate(options)Demande un glissement. Renvoie un SensorDrag, ou null quand aucun glissement ne peut démarrer.
cancel()Annule le glissement ou l'appui en attente du provider.

activate() accepte :

OptionSignification
targetLà où l'entrée s'est posée : un élément à l'intérieur d'un élément déplaçable, ou l'id d'un élément déplaçable. Un élément est vérifié comme un vrai appui (poignée, pas d'enfant interactif) ; un id saute les vérifications.
pointLà où le glissement commence, en coordonnées du viewport. Par défaut : le centre de l'élément déplaçable.
type'pointer' (par défaut) : suit des points, teste les collisions avec les cibles, défile automatiquement. 'keyboard' : se déplace d'item en item, rend les cibles visibles, rend le focus à la fin.
activationUne contrainte ({ delay: 300 }), ou un type de pointeur ('touch') pour utiliser celle qui est configurée. Par défaut : aucune, le glissement démarre tout de suite.
eventL'événement d'entrée, transmis au canDrag de l'item (sans lui, canDrag n'est pas interrogé). Un pointerType tactile en fait un glissement tactile (vibrations).
onStart(drag)Le glissement a démarré, après son activation.
onEnd(drag)Le glissement est terminé : interrompu, déposé ou annulé — par qui que ce soit.
onResume(drag)onBeforeDrop a répondu 'continue' : le glissement continue, et move, drop et cancel s'appliquent de nouveau.

Le SensorDrag qu'elle renvoie :

MembreCe qu'il fait
id, typeL'id de l'item glissé, et 'pointer' ou 'keyboard'.
state'pending' (en attente de l'activation), 'dragging', puis 'ended'.
startedLe glissement a démarré, même s'il s'est terminé depuis.
move(point)L'entrée s'est déplacée. En attente, vérifie la tolérance ; pendant le glissement, déplace le glissement.
step(direction)Glissements au clavier : un pas de flèche, 'up' | 'down' | 'left' | 'right'.
place({ list, index })Place l'item à une position d'une liste triable, comme le font les déplacements au clavier. Se résout à false quand aucune liste ne l'accepte à cet endroit.
drop()Dépose. Se résout à true quand le dépôt a abouti, une fois qu'il a atterri.
cancel()Annule : l'item revient à sa place, ou un appui en attente est abandonné.

Les appels qui ne s'appliquent plus — un déplacement après la fin, un second drop() — sont ignorés. Les types complets : Capteurs.

Pour envelopper un capteur intégré, exécutez son setup avec un contexte à vous. Créez-le avec Object.create(context, { activate: { value: myActivate } }) plutôt qu'avec une décomposition, pour que les getters en direct (status, drag, props) continuent de fonctionner.

Glissements programmatiques

@vue-dnd-kit/actions déplace des items depuis le code : boutons « Monter », menus, raccourcis clavier, annulation, visites guidées, tests. Il est construit sur l'API publique des capteurs, et ses glissements sont de vrais glissements : annoncés et animés, passant par onBeforeDrop, onDrop, e.result et preventDefault(), sous les règles du provider (un glissement à la fois, items désactivés, accept et group.put).

Live

Chargement de la démo…

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

<template>
  <!-- @vue-dnd-kit/actions moves the rows from code: buttons, a shuffle, a scripted drag. -->
  <DnDProvider>
    <Queue />
  </DnDProvider>
</template>
npm i @vue-dnd-kit/actions

moveItem : un appel, un déplacement

import { useDnDActions } from '@vue-dnd-kit/actions';

const { moveItem } = useDnDActions(); // in setup, under a provider

await moveItem('card-1', { index: 0 }); // to the top of its list
await moveItem('card-1', { list: 'done' }); // to the end of another list
await moveItem('card-1', { list: 'done', index: 2 }); // to a place in another list
  • list est l'id d'une liste triable. Sans lui, l'item reste dans sa liste.
  • index est l'endroit où l'item finit, compté sans l'item lui-même, et ramené dans les limites de la liste. Sans lui, l'item va à la fin.
  • Il se résout à true quand le dépôt a abouti, à false quand l'item n'est pas rendu ou est désactivé, qu'aucune liste ne l'accepte à cet endroit, ou que le dépôt a été refusé.
  • Les appels sont mis en file : chacun attend que le provider soit au repos, pour qu'un second clic rapide ne soit pas perdu. Le mélange de la démo, ce sont cinq appels d'affilée.
  • Il s'exécute comme un glissement au clavier : il est donc annoncé et le focus reste sur l'item.
  • Listes uniquement : les positions dans un arbre ont aussi une profondeur.

Voir l'accessibilité pour une ligne avec des boutons de déplacement.

startDrag : un glissement scripté

const { startDrag } = useDnDActions();

const drag = startDrag('card-1'); // null when it can't start now
if (drag) {
  await drag.moveTo(document.querySelector('#trash')!); // onto an element
  await drag.moveTo({ x: 400, y: 120 }); // to a point, in viewport coordinates
  await drag.moveTo({ list: 'done', index: 2 }); // to a list position
  await drag.drop(); // true when the drop went through; or drag.cancel()
}

moveTo se résout une fois le déplacement rendu, drop une fois le dépôt atterri. Le bouton « Play a scripted drag » de la démo fait remonter la dernière piste par petits pas de moveTo, puis la dépose en haut. startDrag(id, { from: { x, y } }) saisit l'item à un point donné au lieu de son centre.

Confier un glissement au clavier

startDrag(id, { type: 'keyboard' }) soulève l'item comme un glissement au clavier. Votre code peut le déplacer avec await drag.step('down'), ou le laisser à l'utilisateur : le capteur clavier pilote chaque glissement au clavier, peu importe qui l'a démarré. Cela donne un élément de menu « Déplacer » :

Live

Chargement de la démo…

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

<template>
  <!-- The ⋯ menus move rows with @vue-dnd-kit/actions. -->
  <DnDProvider>
    <Planner />
  </DnDProvider>
</template>
Row.vue
const { moveItem, startDrag } = useDnDActions();

// One move in one call: picked up, put there and dropped, announced and animated.
const toOther = () => moveItem(props.task.id, { list: props.other.id, index: 0 });
// A keyboard drag, handed to the arrow keys: Space or Enter drops, Escape cancels.
const withKeys = () => {
  el.value?.focus();
  startDrag(props.task.id, { type: 'keyboard' });
};

Les flèches déplacent la ligne d'emplacement en emplacement, ArrowLeft et ArrowRight vers l'autre semaine (les listes partagent un group). Quand le glissement se termine, le focus revient sur la ligne. Les indications de touches sous le tableau s'éclairent tant que useDnD().active.value?.sensor vaut 'keyboard'.

En dehors des composants

useDnDActions() fait tourner un capteur, nommé actions, dans le provider au-dessus. Pour piloter un provider depuis un store ou un harnais de test, ou sous un nom à vous, créez les actions dans un capteur que vous faites tourner vous-même :

import { defineSensor } from '@vue-dnd-kit/core';
import { createDnDActions, type DnDActions } from '@vue-dnd-kit/actions';

export let tour: DnDActions | undefined;
export const tourSensor = defineSensor({
  name: 'tour',
  setup: (context) => void (tour = createDnDActions(context)),
});
<DnDProvider :sensors="[pointerSensor, keyboardSensor, tourSensor]">

Ensuite, await tour?.moveItem('card-1', { list: 'done' }) depuis n'importe où. Pendant le rendu serveur, rien ne glisse : moveItem se résout à false et startDrag renvoie null.