ドキュメント/ガイド

プロバイダー

DnDProvider は配下のすべてのドラッグを管理します。ターゲットを見つけ、ドラッグを開始し、スクロールし、起きたことをすべて useDnD と useDnDMonitor に伝えます。

このページを編集

このページは AI によって翻訳されており、不正確な箇所が含まれている可能性があります。 英語の原文を読む

アプリにプロバイダーを 1 つ

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

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

DnDProvider を App.vue やレイアウトなど上位の階層に 1 つ置けば、あとは気にする必要はありません。すべてのコンポーザブル(makeSortableList、makeDraggable、useDnD…)は、自分が実行されるコンポーネントより上にある最も近いプロバイダーを探します。

コンポーザブルは、プロバイダーの内側に描画されるコンポーネントで実行する必要があります。自身のテンプレートに <DnDProvider> を含むコンポーネントで呼び出すと、No <DnDProvider> found above this component がスローされます。Vue の inject は祖先しか参照できないためです。

プロバイダーが管理するもの

パーツプロバイダーの役割
入力ポインター・タッチ・キーボードの入力をドラッグに変換します(sensors、activation)。ドラッグは一度に 1 つだけです。
ターゲットの検出ドラッグの下にあるドロップターゲットをヒットテストし(collision)、それらに通知します。
オーバーレイポインターに追従するボックスを body にテレポートして描画し、ドロップ時に着地させます。
選択選択中の id の集合を 1 つ持ち、配下のすべてのリストとドラッグ可能な要素で共有します。
自動スクロールドラッグが端に近づくと、コンテナーやウィンドウをスクロールします。
ライブリージョン持ち上げ、移動、ドロップ、キャンセルをスクリーンリーダーに読み上げます。

それ以外は何も描画しません。マークアップはそのままです。

オプションの概要

どのオプションにもそのまま使えるデフォルトがあるので、プロバイダーには通常何も渡しません。プロバイダーは props を必要になった時点で読み取ります。下のデモでは collision、activation、autoScroll をその場で切り替えています。

Propデフォルト設定する内容
collisionpointerFirstドラッグの下のターゲットをどう選ぶか。後述。
activationマウス 4 px、タッチ 200 ms押下がいつドラッグになるか。ポインターの種類ごと。後述。
autoScrollオン端でのスクロール:{ threshold, maxSpeed, acceleration } または false。後述。
hapticsタッチで持ち上げたときの短い振動タッチドラッグの振動、または false。後述。
copyModifier'Alt'ドラッグ中に押していると、移動がコピーになるキー。
modifiers[]すべてのドラッグに適用する移動のルール:制約を参照。
animation / dropAnimationオン並べ替え中にアイテムがスライドする動き / オーバーレイの着地:アニメーションを参照。
animateChangesオフリストとツリーのデフォルト:ドラッグ以外によるデータの変化をアニメーションします。
keyboardSpace / Enter、Escapeキーボードのキーとステップ。false でキーボードドラッグをオフにします。アクセシビリティを参照。
sensorsポインター + キーボード入力をドラッグに変換するもの:センサーを参照。
overlayTo'body'オーバーレイのテレポート先。
overlayFit'slot'オーバーレイのサイズ:着地する枠のサイズか、元の要素のサイズか。オーバーレイを参照。
overlayTransitionクロスフェードドラッグ中にオーバーレイがあるコンポーネントから別のコンポーネントへ変わる仕方:false、または <Transition> の props。オーバーレイを変えるターゲットを参照。
overlayClass—オーバーレイのコンテンツ([data-dnd-overlay-content])に付けるクラス。全体としてアニメーションさせる要素です:ドラッグで持ち上げるときに浮かせ、着地するときに落ち着かせます。オーバーレイ:持ち上げ、傾け、落ち着かせるを参照。
overlayPreviews5マルチドラッグ:持ち上げた時点でクローンを作る、ドラッグ中のアイテムの数、または 'all'。デフォルトのオーバーレイを参照。
announcements組み込みスクリーンリーダー向けのテキスト、または false。
screenReaderInstructions組み込みすべてのドラッグ可能な要素が aria-describedby で参照するテキスト。
defaultStylestrueドラッグの状態用に詳細度ゼロの CSS を挿入します:スタイリングを参照。

すべての型を含む完全な一覧は DnDProvider API にあります。プロバイダーは drag-start、drag-move、drag-over、drag-end、drag-cancel、drag-resume、drag-settle も発行します。useDnDMonitor が受け取るのと同じイベントです。

ドラッグを読み取る:useDnD()

Live

デモを読み込み中…

<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() はドラッグをリアクティブな状態として返します。プロバイダー配下のどのコンポーネントでも使えます。

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

const { status, active, over, pointer, activation, isAutoScrolling, selection, cancel } = useDnD();
フィールド内容
status'idle'、'activating'(押下がアクティベーションを待っている)、'dragging'、'dropping'(ドラッグは実行されていないが、オーバーレイがまだ着地中、または非同期の onBeforeDrop が判断中)、'selecting'(範囲選択中)。
activeドラッグされているもの:id、ids(ドラッグ中のすべての id。掴んだものが先頭)、items、data、type、element、rect、sensor。アイドル時は null。
overドラッグの下にあるターゲット:id、data、element、rect、accepted。何もない上では null。
pointerビューポート座標でのポインター位置。モディファイア適用後の値です。
activation.progress押下がアクティベーションを待っている間、0 から 1 まで進みます。リングを描くのに使えます。
isAutoScrolling自動スクロールが何かを動かしています。
selectionプロバイダーの選択:ids、has、add、delete、toggle、set、clear。マルチドラッグを参照。
cancel()実行中のドラッグ(アイテムは元の位置に戻ります)、チャージ中の押下、または範囲選択をキャンセルします。
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>

ドロップが確定した時点で、オーバーレイがまだ着地中でも(status は 'dropping')、active、over、pointer は null になります。これらから派生した UI が、古いドラッグを新しいデータと並べて表示することはありません。非同期の onBeforeDrop を待っている間は、状態は離した時点のまま保たれます。

次のドラッグは着地を待ちません:すぐに始まり、直前のオーバーレイがその周りで着地している間、status は 'dragging' です。着地中のオーバーレイを押すと、飛んでいる途中で捕まえられます:ドラッグはオーバーレイのいる位置から続きます。ドラッグのオーバーレイが落ち着いたことは onDragSettle が知らせます。

すべてのドラッグを監視する: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
});

モニターは、どこで始まったかに関係なく、プロバイダー配下のすべてのドラッグを受け取ります。上のパネルはリストをインポートせずに、そのリストのドラッグを記録しています。モニターは、呼び出したコンポーネントがアンマウントされると停止します。

イベントタイミング
onDragStartドラッグが始まったとき。
onDragMoveドラッグが動いたとき。
onDragOverドラッグの下のターゲットが変わったとき。
onBeforeDropドラッグが離されたとき。その答えがドロップを決めます:ドロップの判断を参照。
onDragEndドロップが成立したとき。並べ替え可能なリストはすでに自身の onDrop を実行し、新しい順序を書き込んでいます。どのターゲットの上でもなく終わった場合、dropped は false です。
onDragCancelEscape、受け入れないターゲットへのドロップ、または onBeforeDrop が 'reject' を返したとき。
onDragResumeonBeforeDrop が 'continue' を返したとき:何もドロップされず、ドラッグが続きます。
onDragSettleドラッグのオーバーレイが着地したとき(または元の位置に戻ったとき、次のドラッグに捕まえられたとき)。onDragEnd または onDragCancel が受け取ったイベントが渡されます。その間にほかのドラッグが始まっていることもあります。

どのイベントも active、over、pointer、delta(開始位置からのオフセット)、position(ドラッグ中のボックスが over の中のどこにあるか。ドラッグ&ドロップを参照)を持ち、並べ替え可能なリストのアイテムなら source も持ちます。完全な型は イベント API にあります。

「ドロップの後」の処理は onDragEnd で行います:保存、選択の解除、アナリティクスなど。リスト自身の onDrop は書き込みの前に実行されるので、まだ古い順序が見えます。

ターゲットの検出

関数を選び、ピンやカードをターゲットの上へドラッグしてみましょう。バッジはそのフレームで関数がターゲットをどう順位付けしたかを示し、緑のものがターゲットです。

Live

デモを読み込み中…

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

ドラッグの各フレームで、プロバイダーは対象となるドロップターゲットを collision に渡し、collision はそれらの id を良い順に返します。ライブラリには 5 つの関数が付属しています。

関数順位付けの対象向いている用途
pointerFirstポインターの下にあるもの。何もなければ、ドラッグ中のボックスが重なっているもの。デフォルト:ポインターの下では正確で、隙間では寛容です。
pointerWithinポインターの下にあるものだけ。最も内側のものが先頭。正確なターゲット:隙間ではどこにもドロップされません。
rectIntersectionドラッグ中のボックスが重なっているすべてのターゲット:priority が高い順、次にネストが深い順、次に重なりが大きい順。小さなターゲットの上の大きなアイテムで、ポインターがアイテムの端にある場合。
closestCenterすべてのターゲット。ボックスの中心からターゲットの中心までの距離順。遠くても必ずどれかが選ばれます:グリッドや同じサイズのアイテムのリスト。
closestCornersすべてのターゲット。4 つの角の間の距離順。横に並んだ列(カンバン)や、高さが大きく異なるターゲット。

ポインターの下では、ヒットテストはユーザーに見えているものに従います。重なったターゲットのうちヒットするのは一番上のものだけで(重なった部分では front であり、back にはなりません)、入れ子のターゲットでは最も内側のものが先頭になります(subfolder、次に folder)。

独自の関数

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);
};
引数内容
pointerポインター。モディファイア適用後の値です。
overlayRect表示されている状態のドラッグ中のボックス。オーバーレイがなければ null。
candidates対象となるすべてのターゲット:id、rect(スクロールコンテナーで切り取られた、見えている部分)、priority、depth(最上位で 0、ネストが深いほど大きい)。
hitsヒットテストで得られた、ポインターの下にあるターゲット。良い順。

「ターゲットなし」にするには空の配列を返します。無効なターゲット、ドラッグ中のアイテムの内側にあるターゲット、スクロールコンテナーに隠れているターゲットは、関数に渡されません。ドラッグを受け入れないターゲットも候補には残ります。その上では over.accepted が false になり、ドロップは拒否されます(並べ替え可能なアイテムの内側にあるターゲットは代わりに道を譲り、リストがその上で並べ替えられます)。デモでは選んだ関数をラップして順位を記録しています:(args) => (ranked.value = detect(args))。

プロバイダーは、関数が求める切り替えを抑制します。ドラッグが離れたばかりのターゲットは、ポインターが 10 px 動くまで再び選ばれないため、境界でちらつきません。同じ親の中に入れ子になったターゲットの間(カードの列の隙間)では、直前のターゲットが維持されます。

押下がドラッグになるとき

押してもすぐにはドラッグになりません。activation は、ポインターの種類(mouse、touch、pen)ごとに何を待つかを設定します:px 単位の distance、ms 単位の delay(tolerance を超えると押下は中止され、ページをスクロールできます)、またはその両方のうち先に満たされたほうです。

<DnDProvider :activation="{ mouse: { distance: 8 }, touch: { delay: 300, tolerance: 8 } }">
ポインターデフォルト
mouse、pen{ distance: 4 }:クリックはクリックのままです。handle 上でのマウスの押下はすぐに始まり、動かさなければクリックのままです。
touch{ delay: 200, tolerance: 5 }:長押しです。指の下のリストはスクロールできたままになります。

指定しなかった種類はデフォルトのままです。ドラッグ可能な要素自身の activation はドラッグエリアのものより優先され、ドラッグエリアのものはプロバイダーのものより優先されます。押下が待っている間、useDnD().status は 'activating' で、activation.progress は 0 から 1 まで進みます。Escape または useDnD().cancel() で中止できます。制約をその場で試したり、進捗から長押しのリングを描いたりする方法はセンサーにあります。

自動スクロール

チャンネルをリストの上端や下端に向かってドラッグしてみましょう。緑の帯が端のゾーンで、奥に入るほどリストが速くスクロールします。

Live

デモを読み込み中…

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

overflow: auto または scroll の要素はすべて自動でスクロールし、ウィンドウも同様です。プロバイダーはまずポインターの下のコンテナーをスクロールし、次にその祖先、最後にウィンドウをスクロールします。

オプションデフォルト設定する内容
threshold0.2端のゾーン。コンテナーのサイズに対する割合で、24〜96 px の範囲です。
acceleration10一番端での速度(px/フレーム)。ゾーンに入った深さの 2 乗で大きくなるので、出だしはゆっくりで正確です。
maxSpeed25最高速度。端を越えても(ポインターがコンテナーの外にあっても)、速度はこの値まで上がり続けます。

:auto-scroll="false" でオフになります。ある方向へのスクロールは、ポインターがその方向に動いてから始まるので、端の近くで持ち上げたアイテムが勝手にスクロールすることはありません。リスト、ツリー、ドラッグエリアは、自身のコンテナー用に独自の autoScroll を受け取ります。

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

キーボードドラッグは自動スクロールしません。代わりにアイテムが見える位置までスクロールします。何かがスクロールしている間、isAutoScrolling(useDnD() またはドラッグエリアから取得)は true です。

触覚フィードバックとコピーキー

タッチドラッグは開始時に短く振動します。:haptics="false" でオフになり、オブジェクトを渡すと振動ごとに設定できます。

<DnDProvider :haptics="{ start: true, enter: 10, drop: 10, refuse: [20, 40, 20] }">
キー振動するタイミングデフォルト
start長押しがドラッグになったとき。true(10 ms)
enterドラッグを受け入れるターゲットに入ったとき。オフ
dropドロップが成立したとき。オフ
refuseドロップが拒否され、アイテムが元の位置に戻るとき。オフ

それぞれ true(デフォルトのパターン)、ms 単位の長さ、オン/オフのパターン、または false を受け取ります。振動するのはタッチで始まったドラッグだけで、iOS Safari はまったく振動できません。

copyModifier は、リスト間のドラッグ中に押していると移動がコピーになるキーです。デフォルトは 'Alt' で、任意の KeyboardEvent.key、またはなしにする null を指定できます。アイテムの移動元のリストには clone 関数が必要です。アイテムを共有するリストを参照してください。

複数のプロバイダー

ほとんどのアプリに必要なのはちょうど 1 つです。ウィッシュリストを専用のプロバイダーの下に置いてから、本を向こうへ移動してみましょう。本は自分のプロバイダーから出られず、外側のモニターにはウィッシュリストのドラッグが届かなくなります。

Live

デモを読み込み中…

<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>
  • プロバイダーは互いに独立しています。ドラッグが一方から他方へ渡ることはありません。
  • プロバイダーのネストは可能です。最も近い所有者が優先されます:アイテムはその上にある最も近いプロバイダーに属し、外側のプロバイダーはそれを無視します。
  • 各プロバイダーは独自の選択、オーバーレイ、ライブリージョン、モニターを持ちます。

2 つ目のプロバイダーが意味を持つのは、ページの一部がプロバイダー全体に関わる独自の設定(衝突判定、キーボードのキー、読み上げ)を必要とし、かつそのアイテムがそこから出ない場合だけです。ページの 2 つの部分の間でアイテムを移動できないときは、それらが 2 つのプロバイダーの下にないか確認してください。

ページの一部に対するルール(スナップするボード、タッチの押下が長いフィード、何もドラッグできないパネル)には、専用のプロバイダーは必要ありません。
makeDragArea(board, { modifiers: [snapToGrid(20)], activation: { touch: { delay: 400 } } });
そのアイテムはアプリのプロバイダーに属したままで、どこへでも移動できます。制約を参照してください。

別の場所にマウントされたアプリ(shadow root の中や、別の Vue アプリ)も、既存のプロバイダーに参加できます。マウントする前に useDnDContext() を provideDnDContext(app, context) に渡してください。ヒットテストとオーバーレイは shadow root をまたいで動作します。

SSR と Nuxt

プロバイダーは SSR で安全に使えます。リストとアイテムは、ほかのマークアップと同じようにサーバーで描画されます。オーバーレイとライブリージョンはテレポートされ、クライアントでのみマウントされるため、ハイドレーションミスマッチを起こしません。デフォルトの id は Vue の useId() から生成され、サーバーとクライアントで一致します。

Nuxt では、モジュールが DnDProvider をグローバルに登録し、コンポーザブルを自動インポートします。インストールを参照してください。