ドロップが落ちない — 15分で3回直して、原因は3回目だった

公開: 2026-11-11
開発日記JavaScriptPlaywright検証

自作の音楽生成ツール(mini-music-app)に、保管庫の曲を「参考音源」の欄へドラッグして渡す機能を付けました。 ところが、落としても反応しません。2026-11-10の夕方、この件で3回直しました。

この記事は、3回の直しを、実ブラウザで1つずつ再現して振り返る記録です。 先に結論を書きます。

15分で3回直した

コミットの時刻は、17:13、17:22、17:29です。1回目から3回目まで、約15分でした。

2026-11-10 の3回の直し(約15分)17:13 #2358落とし先を広げたアップロード欄ぜんたいで受けるようにした別の穴を塞いだ17:22 #2359dragoverのstopPropagationを外した再現では影響なし17:29 #2360ドラッグ元のeffectAllowedをmoveからcopyMoveへdropが来ない原因各カード下の判定は、このあと書く再現の結果。コミットの時刻はmainに入った時刻(JST)。
3回の直しと、再現で確かめた結果。効いたのは3回目だけだった。

再現する: 必要な部分だけの1ページで、本物のドラッグをする

アプリは音楽生成の画面(Gradio)の上に作ってあり、そのまま調べると要素が多すぎます。 そこで、次の3つだけを置いた1ページを作りました。

動かすのは、PlaywrightのChromium(151.0.7922.34)です。 page.mouse で、押す、動かす、離す、の本物のマウス操作をして、各イベントが何回届いたかを数えます。 16通りの条件を試しました。同じ条件を複数回動かした分は、結果が毎回同じでした(最後に足した3通りだけ、1回です)。

原因: dragoverは来るのに、dropだけ来ない

表の1行目(A)が、修正前の組み合わせです。

ドラッグ元と、ドロップ先の組み合わせ(Chromium 151、実際のドラッグ)ドラッグ元の effectAllowedドロップ先の dropEffectdragoverdropメモmovecopy5回0回修正前(A)copyMovecopy5回1回修正後(B)movemove5回1回設定しないcopy5回1回copymove5回0回dragoverの5回は、この再現でのマウスの動かし方による回数。どの行も同じで、dropだけが変わる。
許可(effectAllowed)と答え(dropEffect)が食い違う行だけ、dropが0回になった。

ドラッグ元は、effectAllowed = "move"(動かすことだけを許す)と宣言していました。 ドロップ先は、dropEffect = "copy"(コピーとして受ける)と答えていました。 この組み合わせだと、ブラウザは drop を発火させません。

怖いのは、dragover は毎回来ることです。

イベントが届いた順dragstartdragover ×5drop修正前move と copydropが来ない修正後copyMove と copydropが来る
修正前も、dragoverは5回来ている。dropだけが来ない。

このアプリは、dragover の中で枠を強調する作りでした。 強調の処理は走るので、見た目は正しく動いているように見えます。落とせないのに、落とせる見た目です。 (枠の強調はコードから読んだことで、当時の画面は見ていません。)

直し方は2つ、再現で確かめました。

2回目の直しは、再現では影響が無かった

2回目は、dragover の中の stopPropagation() を外す直しでした。 stopPropagation は、そのイベントを、ほかの処理に届かなくします。

再現では、dragover で stopPropagation を呼んでも、drop は来ました。capture相でもbubble相でも、同じです。 この直しは、drop が来ない原因ではありませんでした。

ただし、副作用はありました。 dragover で止めると、アップロード欄が自分で持つ dragover の処理が、4回から0回になります。drop は、そのまま1回届きます。

実アプリ(Gradio)で、別の影響があったかは確かめていません。 外したこと自体は、害にはなっていないと思います。

1回目の直しは、別の穴を塞いでいた

1回目は、受ける範囲を、自前の欄から、アップロード欄を含む全体へ広げる直しでした。 再現では、確かに意味がありました。

アップロード欄へ曲を落としたとき、どの処理が呼ばれたか(drop)受ける範囲と止め方自前のdropアップロード欄のdrop自前の欄だけ(1回目の前)0回1回全体へ広げた1回1回広げて、dropをcaptureで受けて止めた1回0回自前の欄だけだと、曲はアップロード欄の既存の処理が受ける。曲だと認識されない。
1回目の直しで、アップロード欄へ落としても曲として受けられるようになった。

自前の欄だけを受け皿にしていると、アップロード欄に落とした曲は、アップロード欄の既存の処理が受けます。自前の処理には届きません。 範囲を広げて、drop をcapture相で受けて stopPropagation すると、自前の処理だけが動きます。

1回目は、この穴を塞いでいました。 ただ、3回目までの間は、この穴を塞いでも、effectAllowedの食い違いが残っていて、落とせませんでした。

自動テストが通っていた理由

直す間も、自動テストは通っていました。

テストは、偽の document と偽のイベントを作り、登録した関数を直接呼ぶ作りです。 ブラウザの「許可と答えが食い違うと drop を発火させない」という判定は、ここには含まれません。

3回目に足したテストは、effectAllowed が "copyMove" であることの確認です。 drop が来ることの確認ではありません。値を確かめているだけで、挙動は確かめていません。

2つの確かめ方の違い偽のイベントで関数を呼ぶ関数の中身は確かめられるブラウザの許可の判定は通らないdropが来るかは分からない実ブラウザで本物のドラッグブラウザの判定を通るイベントの届いた回数を数えられるdropが来るかを確かめられる
今回の食い違いは、右側でしか見つからない。

木の継ぎ手が2つ、わずかにずれて向かい合っている作業台の俯瞰

持ち帰ったこと

まだ確かめていないことが3つあります。

関連する記事です。