センサーは入力をドラッグに変換します。センサーがプロバイダーに伝えるのは「このアイテムをここからドラッグする」ということと、その後の入力の移動先、そしてドロップしたかキャンセルしたかだけです。残りはプロバイダーが、どのセンサーに対しても同じように行います:一度に 1 つのドラッグ、無効なアイテムの拒否、アクティベーション制約、並べ替え、自動スクロール、読み上げ、:kbd{value="Escape"}、そしてドロップ全体です。
組み込みのセンサー
| センサー | 入力 |
|---|---|
pointerSensor | Pointer Events を通じたマウス、タッチ、ペン。ポインターをキャプチャーし、タッチドラッグ中にページがテキストを選択したりスクロールしたりするのを防ぎ、ドラッグの後に続くクリックを握りつぶします。 |
keyboardSensor | Space と Enter で持ち上げてドロップし、矢印キーで動かし、:kbd{value="Escape"} でキャンセルします——どのドラッグでも、ポインタードラッグでも。 |
どちらもデフォルトで動作します。プロバイダーの sensors はこの一覧を置き換えます。
<DnDProvider :sensors="[pointerSensor, keyboardSensor, joystick.sensor]">
キーボードドラッグをオフにするには、keyboardSensor を外すより :keyboard="false" を使ってください。キーボードセンサーは、:kbd{value="Escape"} であらゆるドラッグをキャンセルする役割も担っています。キーは keyboard.keys で割り当て直せます(アクセシビリティを参照)。
アクティベーション制約
押してもすぐにはドラッグになりません。アクティベーション制約を待つので、クリックはクリックのままで、スワイプでもページをスクロールできます。このリストのルールを切り替えて、行を押してみましょう。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Inbox from './Inbox.vue';
</script>
<template>
<DnDProvider>
<Inbox />
</DnDProvider>
</template>制約には 3 つのフィールドがあります。
| フィールド | ドラッグが始まるのは… |
|---|---|
distance | ポインターがその px 数だけ動いたとき。 |
delay | 押下がその ms 数だけ続いたとき。distance もあれば、先に満たされたほう。 |
tolerance | delay と組み合わせて:その間にポインターが動いてよい距離。それを超えると押下は中止され、ページをスクロールできます。distance がなければデフォルトは 5 px。 |
独自の制約がなければ、ポインターの種類ごとにデフォルトがあります。
| ポインター | デフォルト |
|---|---|
| マウス | 4 px 動いた後。handle の上で押した場合は、マウスの制約が設定されていない限りすぐに。 |
| ペン | 4 px 動いた後。 |
| タッチ | 200 ms 押した後。その前に 5 px より大きく動くと中止され、代わりにページがスクロールします。 |
設定する場所
ポインターの種類——mouse、touch、pen——ごとに、プロバイダー、ドラッグエリア、アイテムで設定します。種類ごとに、アイテム自身のものが優先され、次にそのエリアのもの、次にプロバイダーのものです。
<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 } } });
オプションは押下の開始時に読み取られるので、デモのように、ゲッターや ref でその場で切り替えられます。キーボードドラッグはすぐに始まります。独自のセンサーは、自身の制約か、ポインターの種類に設定された制約を求められます。
長押しのリング
押下が待っている間、useDnD().status は 'activating' で、activation.progress は 0 から 1 まで進みます——delay では時間に、distance では動いた距離に応じて。デモは押下が始まった場所にリングを描きます。
<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>
リングには pointer-events: none を付けてください。リングはポインターの下にあり、ドラッグはそこにあるものをヒットテストするからです。中止された押下——tolerance を超えて動いた、または早く離しすぎた——では、status はそのまま 'idle' に戻ります。
ハンドルはすぐに始まる
handle を指定すると、ドラッグを始められるのはハンドルだけになり、その上でのマウスの押下はすぐにドラッグになります。グリップを掴む操作には、区別すべきものがないからです。押した場所で離した押下はクリックのままなので、グリップは click でメニューを開けます(ポインターがキャプチャされるのは、動き始めてからです)。タッチは、ハンドルでページをスクロールできない場合を除き、長押しを待ちます。ハンドルに touch-action: none を、アイテムに短い距離を指定してください。
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 } },
});
アイテム、そのエリア、プロバイダーに設定したマウスの制約は、ハンドルにも適用されます。デモのように、グリップは <button> にしてください:フォーカスを受け取るので、キーボードドラッグもそこから始まります。
一部の押下だけ
canDrag は、ドラッグ可能な要素と並べ替え可能なアイテムに対するもう 1 つの関門です。入力イベントを受け取り、その押下について true か false を答えます。ここでは、ポインタードラッグには Shift が必要で、キーボードはいつもどおりドラッグします。
デモを読み込み中…
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
import Checklist from './Checklist.vue';
</script>
<template>
<DnDProvider>
<Checklist />
</DnDProvider>
</template>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;
},
});
| オプション | 問い合わせのタイミング | 用途 |
|---|---|---|
disabled | 常時。aria-disabled として設定されます | まったく動かせないアイテム:ロックされたもの、読み取り専用のもの。 |
canDrag | 押下ごとに 1 回、そのイベントとともに | 入力に関するルール:押しているキー、ボタン、ポインターの種類。 |
handle | 押下した場所 | 一部分からしかドラッグを始めないアイテム。 |
canDrag はアクティベーション制約の前に、pointerdown または keydown とともに問い合わされます。ポインターの種類も、ルールにできるものの 1 つです。
// Mouse and pen drag; a touch on the row always scrolls the page.
canDrag: (event) => !(event instanceof PointerEvent && event.pointerType === 'touch'),
操作可能な子要素——ボタン、入力欄、チェックボックス、編集可能なフィールド——はポインタードラッグを決して開始しないので、デモのチェックボックスや担当者メニューのように、ドラッグ可能な要素の中でも動作します。handle を指定した場合は、ハンドルだけがドラッグを開始します。
独自のセンサー
ゲームパッド、画面上のジョイスティック、音声コマンド:アイテムを指して動かせるものなら、何でもドラッグを操作できます。このゲームパッドはセンサーです。ヒーローの上で A を押し続けて持ち上げ、パッドで操作し、もう一度 A でドロップ、:kbd{value="B"} でキャンセルします。マウスとキーボードもその隣でそのまま使えます。
デモを読み込み中…
<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 はセンサーに名前と setup を与えます。setup はプロバイダーごとに一度、プロバイダー側のインターフェースを受け取ります。
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;
};
},
});
押下は activate() でプロバイダーにドラッグを求めます。ターゲットは実際の押下と同じようにチェックされます:このプロバイダーのドラッグ可能な要素であり、無効ではなく、その中のボタンでもないことが必要です。
/** 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
};
あとはパッドでドラッグが動き、A か B で終わります。
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();
デモのようにセンサーをプロバイダーの sensors に追加するか、useSensor(joystick.sensor) でコンポーネントが存在する間だけ動かしてください。点はビューポート座標です。デモはカーソルをステージの座標で保持してページと一緒にスクロールさせ、point() で変換しています。
センサーが受け取るもの
setup(context) はプロバイダー側のインターフェースを受け取ります。
| メンバー | 内容 |
|---|---|
props | プロバイダーの props(activation、keyboard…)。変わることがあるので、必要なときに読み取ってください。 |
status | どのセンサーがドラッグを実行しているかに関係なく、プロバイダーのドラッグの状態。 |
drag | どのセンサーが始めたかに関係なく、プロバイダーの現在のドラッグ(待機中または実行中)。なければ null。 |
activate(options) | ドラッグを求めます。SensorDrag を返し、ドラッグを開始できない場合は null を返します。 |
cancel() | プロバイダーのドラッグ、または待機中の押下をキャンセルします。 |
activate() が受け取るもの:
| オプション | 意味 |
|---|---|
target | 入力が当たった場所:ドラッグ可能な要素の中の要素、またはドラッグ可能な要素の id。要素は実際の押下と同じようにチェックされます(ハンドル、操作可能な子要素ではないこと)。id ではチェックを省略します。 |
point | ドラッグが始まる場所。ビューポート座標。デフォルトはドラッグ可能な要素の中心。 |
type | 'pointer'(デフォルト):点に追従し、ターゲットをヒットテストし、自動スクロールします。'keyboard':アイテム単位で動き、ターゲットを表示し、最後にフォーカスを戻します。 |
activation | 制約({ delay: 300 })、または設定済みの制約を使うためのポインターの種類('touch')。デフォルトはなしで、ドラッグはすぐに始まります。 |
event | 入力イベント。アイテムの canDrag に渡されます(なければ canDrag には問い合わせません)。pointerType がタッチなら、タッチドラッグ(触覚フィードバック)になります。 |
onStart(drag) | アクティベーションの後、ドラッグが始まった。 |
onEnd(drag) | ドラッグが終わった:中止、ドロップ、キャンセル——誰によるものでも。 |
onResume(drag) | onBeforeDrop が 'continue' を返した:ドラッグが続き、move、drop、cancel が再び有効になります。 |
返される SensorDrag:
| メンバー | 役割 |
|---|---|
id、type | ドラッグ中のアイテムの id と、'pointer' または 'keyboard'。 |
state | 'pending'(アクティベーション待ち)、'dragging'、そして 'ended'。 |
started | ドラッグが始まった。その後終わっていても。 |
move(point) | 入力が動いた。待機中は tolerance をチェックし、ドラッグ中はドラッグを動かします。 |
step(direction) | キーボードドラッグ:矢印キー 1 回分のステップ、'up' | 'down' | 'left' | 'right'。 |
place({ list, index }) | キーボードでの移動と同じように、アイテムを並べ替え可能なリストの位置に置きます。そこで受け入れるリストがなければ false で解決します。 |
drop() | ドロップします。ドロップが成立すると、着地した後に true で解決します。 |
cancel() | キャンセルします:アイテムは元の位置に戻るか、待機中の押下は破棄されます。 |
もう当てはまらない呼び出し——終了後の移動、2 回目の drop()——は無視されます。完全な型:センサー。
setup を実行してください。コンテキストはスプレッドではなく Object.create(context, { activate: { value: myActivate } }) で作ると、ライブなゲッター(status、drag、props)が動作し続けます。プログラムによるドラッグ
@vue-dnd-kit/actions はコードからアイテムを動かします:「上へ移動」ボタン、メニュー、キーボードショートカット、元に戻す、オンボーディングツアー、テスト。公開のセンサー API の上に作られていて、そのドラッグは本物です:読み上げられ、アニメーションし、onBeforeDrop、onDrop、e.result、preventDefault() を通り、プロバイダーのルール(一度に 1 つのドラッグ、無効なアイテム、accept と group.put)に従います。
デモを読み込み中…
<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:1 回の呼び出しで 1 回の移動
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は並べ替え可能なリストのidです。省略すると、アイテムは自分のリストにとどまります。indexはアイテムが最終的に入る位置で、アイテム自身を除いて数え、リストの範囲に丸められます。省略すると、アイテムは末尾へ移ります。- ドロップが成立すれば
trueで、アイテムが描画されていない・無効である、そこで受け入れるリストがない、ドロップが拒否された場合はfalseで解決します。 - 呼び出しはキューに入ります。それぞれプロバイダーがアイドルになるまで待つので、すばやい 2 回目のクリックも失われません。デモのシャッフルは 5 回の連続した呼び出しです。
- キーボードドラッグとして実行されるので、読み上げられ、フォーカスはアイテムにとどまります。
- リスト専用です:ツリーの位置には深さもあるためです。
移動ボタン付きの行についてはアクセシビリティを参照してください。
startDrag:スクリプトによるドラッグ
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 は移動が描画されると、drop はドロップが着地すると解決します。デモの「Play a scripted drag」は、最後のトラックを小さな moveTo のステップで滑らせるように上げていき、一番上でドロップします。startDrag(id, { from: { x, y } }) は、アイテムの中心ではなく指定した点で持ち上げます。
ドラッグをキーボードに渡す
startDrag(id, { type: 'keyboard' }) はアイテムをキーボードドラッグとして持ち上げます。コードから await drag.step('down') で動かすことも、ユーザーに任せることもできます。キーボードセンサーは、誰が始めたかに関係なく、すべてのキーボードドラッグを操作します。これで「移動」メニューの項目が作れます。
デモを読み込み中…
<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>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' });
};
矢印キーで行が枠ごとに動き、:kbd{value="ArrowLeft"} と ArrowRight で別の週へ移ります(リストは group を共有しています)。ドラッグが終わると、フォーカスは行に戻ります。ボードの下のキーのヒントは、useDnD().active.value?.sensor が 'keyboard' の間、強調表示されます。
コンポーネントの外から
useDnDActions() は、上にあるプロバイダーで actions という名前のセンサーを 1 つ動かします。ストアやテストハーネスから、あるいは独自の名前でプロバイダーを操作するには、自分で動かすセンサーの中で actions を作成してください。
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]">
あとは await tour?.moveItem('card-1', { list: 'done' }) をどこからでも呼べます。サーバーレンダリング中は何もドラッグされません:moveItem は false で解決し、startDrag は null を返します。