Documentação/Guias

Sensores e arrastes programáticos

Como a entrada vira um arraste — os sensores de ponteiro e de teclado, restrições de ativação, canDrag, sensores seus e arrastes que você roteiriza com @vue-dnd-kit/actions.

Editar esta página

Esta página foi traduzida por IA e pode conter imprecisões. Ler o original em inglês

Um sensor transforma a entrada em arrastes. Ele só diz ao provider "arraste este item a partir daqui", depois para onde a entrada se moveu, e se ela soltou ou cancelou. O provider faz o resto, do mesmo jeito para todo sensor: um arraste por vez, itens desativados recusados, restrições de ativação, ordenação, rolagem automática, anúncios, Escape e todo o drop.

Os sensores embutidos

SensorEntrada
pointerSensorMouse, toque e caneta, por Pointer Events. Ele captura o ponteiro, impede que a página selecione texto ou role sob um arraste por toque, e engole o clique que vem depois de um arraste.
keyboardSensorSpace e Enter pegam e soltam, as setas movem, Escape cancela — qualquer arraste, arrastes por ponteiro também.

Os dois rodam por padrão. A prop sensors do provider substitui a lista:

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

Para desligar os arrastes por teclado, prefira :keyboard="false" a deixar keyboardSensor de fora: o sensor de teclado também é o que cancela qualquer arraste com Escape. As teclas dele são redefinidas com keyboard.keys (veja Acessibilidade).

Restrições de ativação

Uma pressão não arrasta na hora: ela espera a sua restrição de ativação, para que um clique continue sendo um clique e um deslizar do dedo ainda possa rolar a página. Troque as regras desta lista e pressione uma linha:

Live

Carregando a demo…

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

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

Uma restrição tem três campos:

CampoO arraste começa…
distanceAssim que o ponteiro se moveu essa quantidade de px.
delayAssim que a pressão durou essa quantidade de ms. Junto com distance, o que vier primeiro.
toleranceCom delay: quanto o ponteiro pode se mover enquanto isso. Além disso, a pressão desiste, para que a página possa rolar. Padrão de 5 px quando não há distance.

Sem uma sua, cada tipo de ponteiro tem o seu padrão:

PonteiroPadrão
MouseDepois de mover 4 px. Na hora quando pressionado em um handle, a menos que uma restrição de mouse esteja definida.
CanetaDepois de mover 4 px.
ToqueDepois de uma pressão de 200 ms. Mover mais de 5 px antes disso faz desistir, e a página rola.

Onde defini-las

Por tipo de ponteiro — mouse, touch, pen — no provider, em uma área de arraste ou no item. Para cada tipo, vence a do próprio item, depois a da área dele, depois a do 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 } } });

As opções são lidas quando uma pressão começa, então um getter ou um ref as troca ao vivo, como faz a demo. Arrastes por teclado começam na hora. Um sensor seu pede a sua própria restrição, ou a configurada para um tipo de ponteiro.

Um anel de toque longo

Enquanto uma pressão espera, useDnD().status é 'activating' e activation.progress vai de 0 a 1 — pelo tempo com delay, pela distância percorrida com distance. A demo desenha um anel onde a pressão começou:

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>

Dê ao anel pointer-events: none: ele fica sob o ponteiro, e o arraste testa o que está ali. Uma pressão que desiste — moveu-se além da tolerance, ou foi solta cedo demais — leva status direto de volta a 'idle'.

Handles começam na hora

Com um handle, só o handle inicia um arraste, e uma pressão do mouse nele arrasta na hora: pegar uma alça não deixa nada a distinguir. Uma pressão solta onde começou continua sendo um clique, então a alça pode abrir um menu no click (o ponteiro só é capturado quando se move). Um toque ainda espera o seu toque longo, a menos que o handle não possa rolar a página. Dê a ele touch-action: none e ao item uma distância curta:

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 } },
});

Uma restrição de mouse definida no item, na área dele ou no provider vale para os handles também. Faça da alça um <button>, como faz a demo: ela recebe o foco, então um arraste por teclado também começa por ela.

Só algumas pressões

canDrag é mais uma barreira em draggables e itens ordenáveis. Ele recebe o evento de entrada e responde true ou false para esta pressão. Aqui, arrastes por ponteiro precisam de Shift; o teclado arrasta como sempre:

Live

Carregando a demo…

<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;
  },
});
OpçãoConsultadaUse para
disabledAo vivo, e definida como aria-disabledItens que não podem se mover de jeito nenhum: trancados, somente leitura.
canDragUma vez por pressão, com o evento delaRegras sobre a entrada: uma tecla segurada, um botão, um tipo de ponteiro.
handleOnde a pressão caiItens que iniciam um arraste só a partir de uma parte.

canDrag é consultado antes da restrição de ativação, com o pointerdown ou o keydown. Um tipo de ponteiro é mais uma regra que ele pode criar:

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

Filhos interativos — botões, inputs, checkboxes, campos editáveis — nunca iniciam um arraste por ponteiro, então continuam funcionando dentro de um draggable, como o checkbox e o menu de responsável da demo. Com um handle, só o handle inicia um.

Um sensor seu

Um gamepad, um joystick na tela, comandos de voz: qualquer coisa que consiga apontar para um item e se mover pode conduzir um arraste. Este gamepad é um sensor. Segure A sobre um herói para pegá-lo, conduza com o direcional, A de novo solta, B cancela. O mouse e o teclado continuam funcionando ao lado dele:

Live

Carregando a demo…

<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 dá ao sensor o seu nome e um setup que recebe o lado do provider, uma vez por 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;
    };
  },
});

Uma pressão pede um arraste ao provider com activate(). O alvo é verificado como uma pressão real: ele precisa ser um draggable deste provider, não desativado, e não um botão dentro de um:

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
};

Depois o arraste se move com o direcional, e A ou B o encerram:

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();

Adicione o sensor aos sensors do provider, como faz a demo, ou rode-o enquanto um componente existir com useSensor(joystick.sensor). Os pontos ficam em coordenadas da viewport: a demo guarda o cursor nas coordenadas do palco, para que ele role com a página, e o converte em point().

O que o sensor recebe

setup(context) recebe o lado do provider:

MembroO que é
propsAs props do provider (activation, keyboard…). Leia-as quando precisar: elas podem mudar.
statusO status do arraste do provider, seja qual for o sensor que conduz o arraste.
dragO arraste atual do provider, pendente ou em andamento, seja qual for o sensor que o iniciou. null quando não há nenhum.
activate(options)Pede um arraste. Retorna um SensorDrag, ou null quando nenhum arraste pode começar.
cancel()Cancela o arraste ou a pressão pendente do provider.

activate() recebe:

OpçãoSignificado
targetOnde a entrada caiu: um elemento dentro de um draggable, ou o id de um draggable. Um elemento é verificado como uma pressão real (handle, nenhum filho interativo); um id pula as verificações.
pointOnde o arraste começa, em coordenadas da viewport. Padrão: o centro do draggable.
type'pointer' (padrão): segue pontos, testa os alvos sob o ponteiro, rola automaticamente. 'keyboard': move por itens, revela os alvos, devolve o foco no fim.
activationUma restrição ({ delay: 300 }), ou um tipo de ponteiro ('touch') para usar a configurada. Padrão: nenhuma, o arraste começa na hora.
eventO evento de entrada, passado ao canDrag do item (sem ele, canDrag não é consultado). Um pointerType de toque faz dele um arraste por toque (vibração).
onStart(drag)O arraste começou, depois da ativação.
onEnd(drag)O arraste acabou: abortado, solto ou cancelado — por quem quer que seja.
onResume(drag)onBeforeDrop respondeu 'continue': o arraste continua, e move, drop e cancel voltam a valer.

O SensorDrag que ele retorna:

MembroO que faz
id, typeO id do item arrastado, e 'pointer' ou 'keyboard'.
state'pending' (esperando a ativação), 'dragging', depois 'ended'.
startedO arraste começou, mesmo que já tenha terminado desde então.
move(point)A entrada se moveu. Enquanto pendente, verifica a tolerância; durante o arraste, move o arraste.
step(direction)Arrastes por teclado: um passo de seta, 'up' | 'down' | 'left' | 'right'.
place({ list, index })Coloca o item em uma posição de uma lista ordenável, como fazem os movimentos por teclado. Resolve false quando nenhuma lista o aceita ali.
drop()Solta. Resolve true quando o drop foi concluído, depois que ele pousou.
cancel()Cancela: o item volta ao lugar, ou uma pressão pendente é abandonada.

Chamadas que não se aplicam mais — um movimento depois do fim, um segundo drop() — são ignoradas. Tipos completos: Sensores.

Para envolver um sensor embutido, rode o setup dele com um contexto seu. Crie-o com Object.create(context, { activate: { value: myActivate } }) em vez de um spread, para que os getters ao vivo (status, drag, props) continuem funcionando.

Arrastes programáticos

@vue-dnd-kit/actions move itens pelo código: botões "Mover para cima", menus, atalhos de teclado, desfazer, tours de onboarding, testes. Ele é construído sobre a API pública de sensores, e os arrastes dele são de verdade: anunciados e animados, passando por onBeforeDrop, onDrop, e.result e preventDefault(), sob as regras do provider (um arraste por vez, itens desativados, accept e group.put).

Live

Carregando a demo…

<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: uma chamada, um movimento

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 é o id de uma lista ordenável. Sem ele, o item fica na sua lista.
  • index é onde o item termina, contado sem o próprio item, e limitado ao tamanho da lista. Sem ele, o item vai para o fim.
  • Resolve true quando o drop foi concluído, false quando o item não está renderizado ou está desativado, nenhuma lista o aceita ali, ou o drop foi recusado.
  • As chamadas entram em fila: cada uma espera até o provider ficar ocioso, então um segundo clique rápido não se perde. O embaralhamento da demo são cinco chamadas seguidas.
  • Ele roda como um arraste por teclado, então é anunciado e o foco fica no item.
  • Só listas: posições em árvores também têm uma profundidade.

Veja Acessibilidade para uma linha com botões de mover.

startDrag: um arraste roteirizado

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 resolve depois que o movimento foi renderizado, drop depois que o drop pousou. O "Play a scripted drag" da demo desliza a última faixa para cima em pequenos passos de moveTo, depois a solta no topo. startDrag(id, { from: { x, y } }) pega o item em um ponto em vez de no centro.

Passar um arraste para o teclado

startDrag(id, { type: 'keyboard' }) ergue o item como um arraste por teclado. O seu código pode movê-lo com await drag.step('down'), ou deixá-lo para o usuário: o sensor de teclado conduz todo arraste por teclado, seja quem for que o iniciou. Isso dá um item de menu "Mover":

Live

Carregando a demo…

<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' });
};

As setas movem a linha de posição em posição, ArrowLeft e ArrowRight para a outra semana (as listas compartilham um group). Quando o arraste termina, o foco volta para a linha. As dicas de teclas embaixo do quadro se acendem enquanto useDnD().active.value?.sensor é 'keyboard'.

Fora de componentes

useDnDActions() roda um sensor, chamado actions, no provider acima. Para conduzir um provider a partir de uma store ou de um harness de testes, ou sob um nome seu, crie as ações em um sensor que você mesmo roda:

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]">

Depois, await tour?.moveItem('card-1', { list: 'done' }) de qualquer lugar. Durante a renderização no servidor nada é arrastado: moveItem resolve false e startDrag retorna null.