dropが来ない時の確認

更新: 2026-11-11
ドラッグ&ドロップJavaScriptPlaywright検証

ドラッグ&ドロップで、dragover は来るのに drop が来ないときの確認手順です。 実際に16通りの条件でドラッグして、イベントが何回届くかを数えた結果をまとめます。 経緯は開発日記のドロップが落ちない 15分で3回直したに書きました。

測った環境は、PlaywrightのChromium 151.0.7922.34で、page.mouse による本物のマウス操作です。

1 見る順番

次の順に見ます。上の項目から順に、違っていないかを確かめます。

確認すること(この順に)違っていたとき1 dragoverの中でpreventDefaultを呼んでいるか2 effectAllowedとdropEffectは許可の組み合わせか3 落とした場所は受ける範囲に入っているか4 別の処理が先にdropを受けていないか呼んでいないと、dropは来ない(N)dragoverの中で呼ぶ食い違うと、dropは来ない(A・E)dropEffectは設定しない(O)外だと、別の処理が受ける(H)closestで受ける範囲を広げる(I)先に受ける処理があるときdropをcaptureで受けて止める(K)
丸かっこの記号は、下の「測った16通り」の記号。1から順に確かめる。

2 effectAllowedとdropEffectの組み合わせ

effectAllowed は、ドラッグ元が dragstart で宣言する「許す操作」です。 dropEffect は、ドロップ先が dragover で答える「どの操作で受けるか」です。

この2つが食い違うと、dragover は来続けるのに、drop だけが来ません。 測った結果は次のとおりです。dragover は、どの行も5回でした(この再現でのマウスの動かし方による回数です)。

  • A: effectAllowed = "move"、dropEffect = "copy" → drop 0回
  • B: effectAllowed = "copyMove"、dropEffect = "copy" → drop 1回
  • C: effectAllowed = "move"、dropEffect = "move" → drop 1回
  • D: effectAllowed を設定しない、dropEffect = "copy" → drop 1回
  • E: effectAllowed = "copy"、dropEffect = "move" → drop 0回
  • O: effectAllowed = "move"、dropEffect を設定しない → drop 1回。drop の dropEffect は "move"
  • P: どちらも設定しない → drop 1回。dropEffect は "copy"
  • N: dragover で preventDefault を呼ばない → drop 0回

迷ったら、dropEffect を設定しない(O・P)のが安全でした。 コピーのカーソルを出したいなど、dropEffect を決めたい理由があるときは、ドラッグ元の effectAllowed も合わせます(B)。

dragover が来ていても、drop が来る保証にはなりません。 dragover の中で枠を強調する作りだと、見た目が正常なまま、落とせません。

3 受ける範囲とstopPropagation

アップロード欄のように、自前とは別に、dragover と drop の処理を持つ部品の上へ落とす場合の結果です。

  • H: 受ける範囲が自前の欄だけ(アップロード欄は範囲の外)
  • I: 範囲を、アップロード欄を含む全体に広げた
  • K: 広げて、drop を、capture相で受けて stopPropagation した
  • L: 広げて、dragover を、capture相で受けて stopPropagation した
  • M: 広げて、dragover と drop の両方を止めた
アップロード欄へ曲を落としたとき、各処理が呼ばれた回数条件自前のdrop相手のdragover相手のdropH 範囲が自前の欄だけ041I 範囲を広げた141K 広げて、dropを止めた140L 広げて、dragoverを止めた101M 広げて、両方止めた100相手 = アップロード欄の代役。自前とは別に、dragoverとdropの処理を持つ。止めたのはcapture相。
止めた処理だけが0回になる。dragoverを止めても、dropは届く。

わかったことは3つです。

  • 範囲が狭いと、別の部品の既存の処理が曲を受ける(H)。受ける範囲は、event.target.closest(...) で親の要素まで広げる
  • 相手の既存の drop を動かしたくないときは、drop を、capture相で受けて stopPropagation する(K)
  • dragover で stopPropagation しても、drop は止まらない。ただし、相手の dragover の処理が呼ばれなくなる(L)。自前の欄へ落とすときに dragover を止めても、drop は来た(F・G)

4 再現の書き方

ページ側は、次の3つだけでも再現できます。

row.addEventListener("dragstart", (e) => {
  e.dataTransfer.setData("application/x-track", "曲A.wav");
  // effectAllowed は設定しない
});
document.addEventListener("dragover", (e) => {
  if (!e.target.closest("#zone")) return;
  e.preventDefault(); // これが無いとdropは来ない(N)
}, true);
document.addEventListener("drop", (e) => {
  if (!e.target.closest("#zone")) return;
  e.preventDefault();
  window.dropCount = (window.dropCount || 0) + 1;
}, true);

ドラッグは、Playwrightの page.mouse で、押す、動かす、離す、の順に行います。 この方法で、ブラウザの許可の判定(AとE)まで再現できました。

const src = await page.locator("#row").boundingBox();
const dst = await page.locator("#zone").boundingBox();
await page.mouse.move(src.x + 10, src.y + 10);
await page.mouse.down();
await page.mouse.move(dst.x + 20, dst.y + 10, { steps: 8 });
await page.mouse.move(dst.x + 30, dst.y + 12, { steps: 3 });
await page.mouse.up();
console.log(await page.evaluate(() => window.dropCount));

条件は、URLの ?ea=move&de=copy のように、クエリで切り替えられるようにしておくと、16通りを1つのスクリプトで回せます。 測った記録は、各条件の dragover、drop、相手の dragover、相手の drop の回数です。

5 偽のイベントのテストでは見つからない

イベントを、偽の document と偽の dataTransfer で作って、登録した関数を直接呼ぶテストは、関数の中身を確かめられます。 ただし、ブラウザが行う「許可と答えが食い違うと drop を発火させない」という判定は通りません。

同じ食い違いを、偽のイベントのテストで見つけることはできませんでした(今回の実例では、テストは通ったままでした)。 effectAllowed の値を確かめるテストを足すだけでは、drop が来ることの確認にはなりません。

「直した」の確認は、本物のドラッグで、drop が来た回数を数えて行います。

6 確かめていない範囲

  • Chrome以外(Firefox、Safari)の挙動。上の結果はChromium 151だけです
  • タッチ操作。マウスでのドラッグだけを測りました
  • 実アプリ(Gradio)の画面での確認。代役のページで測りました
  • effectAllowed の、ほかの値(link など)の組み合わせ

関連

WIKI一覧へ